├── .gitignore ├── LICENSE.txt ├── META-INF └── MANIFEST.MF ├── NOTICE.txt ├── README.md ├── build.properties ├── pom.xml └── src ├── main └── java │ └── com │ └── amazonaws │ └── services │ └── dynamodbv2 │ └── streamsadapter │ ├── AdapterRequestCache.java │ ├── AmazonDynamoDBStreamsAdapterClient.java │ ├── DynamoDBStreamsConsumerStates.java │ ├── DynamoDBStreamsDataFetcher.java │ ├── DynamoDBStreamsPeriodicShardSyncManager.java │ ├── DynamoDBStreamsProxy.java │ ├── DynamoDBStreamsShardConsumer.java │ ├── DynamoDBStreamsShardConsumerFactory.java │ ├── DynamoDBStreamsShardSyncer.java │ ├── DynamoDBStreamsShutdownTask.java │ ├── StreamsDeterministicShuffleShardSyncLeaderDecider.java │ ├── StreamsLeaseCleanupValidator.java │ ├── StreamsMultiLangDaemon.java │ ├── StreamsRecordProcessor.java │ ├── StreamsWorkerFactory.java │ ├── exceptions │ └── UnableToReadMoreRecordsException.java │ ├── leases │ └── StreamsLeaseTaker.java │ ├── model │ ├── AmazonServiceExceptionTransformer.java │ ├── DescribeStreamRequestAdapter.java │ ├── DescribeStreamResultAdapter.java │ ├── GetRecordsRequestAdapter.java │ ├── GetRecordsResultAdapter.java │ ├── GetShardIteratorRequestAdapter.java │ ├── GetShardIteratorResultAdapter.java │ ├── ListStreamsRequestAdapter.java │ ├── ListStreamsResultAdapter.java │ ├── RecordAdapter.java │ ├── RecordObjectMapper.java │ ├── ShardAdapter.java │ └── StreamDescriptionAdapter.java │ └── utils │ ├── Sleeper.java │ └── ThreadSleeper.java └── test └── java └── com └── amazonaws └── services ├── dynamodbv2 └── streamsadapter │ ├── AdapterRequestCacheTests.java │ ├── AmazonDynamoDBStreamsAdapterClientTest.java │ ├── DynamoDBStreamsConsumerStatesTest.java │ ├── DynamoDBStreamsDataFetcherTest.java │ ├── DynamoDBStreamsPeriodicShardSyncManagerTest.java │ ├── DynamoDBStreamsProxyImportedFromKinesisProxyTest.java │ ├── DynamoDBStreamsProxyTest.java │ ├── DynamoDBStreamsShardConsumerTest.java │ ├── DynamoDBStreamsShardSyncerTest.java │ ├── DynamoDBStreamsShutdownTaskTest.java │ ├── PeriodicShardSyncTestBase.java │ ├── StreamsDeterministicShuffleShardSyncLeaderDeciderTest.java │ ├── StreamsRecordProcessorTest.java │ ├── functionals │ ├── CorrectnessTest.java │ ├── FunctionalTestBase.java │ └── KinesisParametersTest.java │ ├── leases │ └── StreamsLeaseTakerTest.java │ ├── model │ ├── AmazonServiceExceptionTransformerTests.java │ ├── DescribeStreamRequestAdapterTest.java │ ├── DescribeStreamResultAdapterTest.java │ ├── GetRecordsRequestAdapterTest.java │ ├── GetRecordsResultAdapterTest.java │ ├── GetShardIteratorRequestAdapterTest.java │ ├── GetShardIteratorResultAdapterTest.java │ ├── ListStreamsRequestAdapterTest.java │ ├── ListStreamsResultAdapterTest.java │ ├── RecordAdapterTest.java │ ├── ShardAdapterTest.java │ └── StreamDescriptionAdapterTest.java │ └── util │ ├── CountingRecordProcessor.java │ ├── ExceptionThrowingLeaseManager.java │ ├── KinesisLocalFileDataCreator.java │ ├── KinesisLocalFileProxy.java │ ├── RecordProcessorTracker.java │ ├── ReplicatingRecordProcessor.java │ ├── ShardObjectHelper.java │ ├── TestRecordProcessorFactory.java │ └── TestUtil.java └── kinesis └── clientlibrary └── lib └── worker └── KinesisClientLibraryRecordDeserializationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | AwsCredentials.properties 3 | /.idea 4 | *.iml 5 | *~ 6 | .*~ 7 | -------------------------------------------------------------------------------- /META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Bundle-ManifestVersion: 2 3 | Bundle-Name: Amazon DynamoDB Streams Kinesis Adapter for Java 4 | Bundle-SymbolicName: com.amazonaws.dynamodb.streams.kinesis.adapter;singleton:=true 5 | Bundle-Version: 1.5.0 6 | Bundle-Vendor: Amazon Technologies, Inc 7 | Bundle-RequiredExecutionEnvironment: JavaSE-1.8 8 | Export-Package: com.amazonaws.services.dynamodbv2.streamsadapter, 9 | com.amazonaws.services.dynamodbv2.streamsadapter.exceptions, 10 | com.amazonaws.services.dynamodbv2.streamsadapter.model 11 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | DynamoDB Streams Kinesis Adapter for Java 2 | Copyright 2014-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DynamoDB Streams Kinesis Adapter for Java 2 | 3 | **DynamoDB Streams Kinesis Adapter** implements the Amazon Kinesis interface so that your application can use KCL to consume and process data from a DynamoDB stream. You can get started in minutes using ***Maven***. 4 | 5 | * [DynamoDB Streams Developer Guide][docs-dynamodb-streams] 6 | * [Amazon Kinesis Client Library GitHub and Documentation][docs-kcl] 7 | * [DynamoDB Forum][dynamodb-forum] 8 | * [DynamoDB Details][dynamodb-details] 9 | * [Issues][adapter-issues] 10 | 11 | ## Features 12 | 13 | * The DynamoDB Streams Kinesis Adapter for Amazon Kinesis Client Library (KCL) is the best way to ingest and process data records from DynamoDB Streams. 14 | * The KCL is designed to process streams from Amazon Kinesis, but by adding the DynamoDB Streams Kinesis Adapter, your application can process DynamoDB Streams instead, seamlessly and efficiently. 15 | 16 | ## Release Notes 17 | 18 | ### Latest Release (v1.6.1) 19 | * Upgrades AWS Java SDK to version 1.12.710. 20 | * Adds dependency on Lombok 1.18.32. 21 | * Falling back to Maven Central repository for DynamoDBLocal library. 22 | 23 | ### Release (v1.6.0) 24 | * Upgrades Amazon Kinesis Client Library (KCL) to version 1.14.9. Customers can now use DynamoDB Streams Adapter with KCL version 1.14.9. However, DynamoDB Streams Adapter does not inherit performance optimizations like support for child shards, shard synchronization, deferred lease clean-up available in KCL. 25 | * Fixes the [bug](https://github.com/awslabs/dynamodb-streams-kinesis-adapter/issues/40) which was causing errors in DynamoDB Streams Adapter with KCL version 1.14.0. 26 | * With upgrade to KCL version 1.14.9, the default shard prioritization strategy has been changed to `NoOpShardPrioritization`. To retain the existing behavior, DynamoDB Streams customers should explicitly update the shard prioritization strategy to `ParentsFirstShardPrioritization` if there was no explicit override done in the application. 27 | * Upgrades jackson-databind to version 2.12.7.1 28 | * This release uses Apache 2.0 license. 29 | 30 | ### Release (v1.5.4) 31 | * Upgrades AWS Java SDK to version 1.12.130 32 | * Upgrades jackson-databind to version 2.12.6.1 33 | * Fixes logging in `DynamoDBStreamsShardSyncer` to log only the problematic shardId instead of logging all the shardIds 34 | 35 | 36 | ### Release (v1.5.3) 37 | * Upgrades jackson-databind to version 2.9.10.7 38 | * Upgrades junit to version 4.13.1 39 | * Upgrades AWS Java SDK to version 1.11.1016 40 | 41 | ### Release (v1.5.2) 42 | * Upgrades jackson-databind to version 2.9.10.5 43 | * Updates `StreamsWorkerFactory` to use `KinesisClientLibConfiguration` billing mode when constructing `KinesisClientLeaseManager`. 44 | 45 | ### Release (v1.5.1) 46 | * Restores compile compatibility with KCL 1.13.3. 47 | * Fixes a performance issue that arised when using v1.5.0 with KCL 1.12 through 1.13.2. 48 | * Fixes a defect where `MaxLeasesForWorker` configuration was not being propagated to `StreamsLeaseTaker`. 49 | * Finished (SHARD_END) leases will now only be delete after at least 6 hours have passed since the shard was created. This further reduces the chances of lineage replay. 50 | 51 | ### Release (v1.5.0) 52 | * Introduces the implementation of periodic shard sync in conjunction with Amazon Kinesis Client Library v1.11.x (KCL). The default shard sync strategy is to discover new/child shards only when a consumer completes processing a shard. This default strategy constrains horizontal scaling of customer applications when consuming tables with 10,000+ partitions due to increased DescribeStream calls. Periodic shard sync guarantees that only a subset of the fleet (by default 10) will perform shard syncs, and decouples DescribeStream call volume from growth in fleet size. 53 | 54 | * Improves inconsistency handling in DescribeStream result aggregation by fixing any parent-open-child-open cases. This ensures that shard sync does not fail due to an assertion failure in KCL on this type of inconsistency. 55 | 56 | * Modifies finished shard lease cleanup mechanism. Leases for shards that have been completely processed are now deleted only after all their children shards have been completely processed. This will prevent shard lineage replay issues, instances of which have been reported in the past by some customers. 57 | 58 | * Introduces `StreamsLeaseTaker` with improved load-balancing of leases among workers. 59 | * SHARD_END and non-SHARD_END check-pointed leases are balanced independently. 60 | * Leases are now stolen evenly from other workers instead of from only the most loaded worker. `MaxLeasesToStealAtOneTime` no longer needs to be specified by users. It is now determined automatically based on the number of leases held by the worker. The user-specified value for this is no longer used. 61 | 62 | * Users should continue using factory methods from `StreamsWorkerFactory` to create KCL Worker as specified in the guidance of Release v1.4.x. 63 | * We strongly recommended that you create only one worker per host in your processing fleet to get optimal performance from DynamoDB Streams service. 64 | 65 | ### Release (v1.4.x) 66 | * This release fixes an issue of high propagation delay of streams records when processing streams on small tables. This issue occurs when KCL ShardSyncer is not discovering new shards due to server side delays in shard creation or in reporting new shard creation to internal services. The code is implemented in a new implementation of IKinesisProxy interface called DynamoDBStreamsProxy which is part of the latest release. 67 | * This release requires Kinesis Client Library version >= 1.8.10. Version 1.8.10 has changes to allow IKinesisProxy injection into the KCL Worker builder which is required by DynamoDB Streams Kinesis Adapter v1.4.x for 68 | injection of DynamoDBStreamsProxy into the KCL worker during initialization. Please refer to [Kinesis Client Library release notes for 1.8.10](https://github.com/awslabs/amazon-kinesis-client/blob/master/CHANGELOG.md#release-1810) for more information. 69 | * Suggested AWS Java SDK version >= 1.11.218 70 | * It is highly recommended to [configure][kcl-configuration] Kinesis Client Library with `MaxRecords = 1000` and `IdleTimeInMillis = 500` to optimize DynamoDB Streams costs. 71 | 72 | ### Guidance for injecting DynamoDBStreamsProxy into KCL worker when using DynamoDB Streams Kinesis Adapter v1.4.x. 73 | To fix high propagation delay problems, opt-into using DynamoDBStreamsProxy (instead of the default KinesisProxy) by using the StreamsWorkerFactory factory method (shown below). This injects an instance of DynamoDBStreamsProxy into the created KCL worker. 74 | ``` 75 | final Worker worker = StreamsWorkerFactory 76 | .createDynamoDbStreamsWorker( 77 | recordProcessorFactory, 78 | workerConfig, 79 | adapterClient, 80 | amazonDynamoDB, 81 | amazonCloudWatchClient); 82 | ``` 83 | 84 | ## Getting Started 85 | 86 | 1. **Sign up for AWS** - Before you begin, you need an AWS account. Please see the [AWS Account and Credentials][docs-signup] section of the developer guide for information about how to create an AWS account and retrieve your AWS credentials. You don’t need this if you’re using DynamoDB Local. 87 | 1. **Minimum requirements** - To run the SDK you will need **Java 1.8+**. For more information about the requirements and optimum settings for the SDK, please see the [Java Development Environment][docs-signup] section of the developer guide. 88 | 1. **Install the DynamoDB Streams Kinesis Adapter** - Using ***Maven*** is the recommended way to install the DynamoDB Streams Kinesis Adapter and its dependencies, including the AWS SDK for Java. To download the code from GitHub, simply clone the repository by typing: `git clone https://github.com/awslabs/dynamodb-streams-kinesis-adapter.git`, and run the Maven command described below in "Building From Source". You may also depend on the maven artifact [com.amazonaws:dynamodb-streams-kinesis-adapter][adapter-maven]. 89 | 1. **Build your first application** - There is a walkthrough to help you build first application using this adapter. Please see [Using the DynamoDB Streams Kinesis Adapter to Process Stream Records][docs-adapter]. 90 | 91 | ## Including as a Maven dependency 92 | 93 | Add the following to your Maven pom file: 94 | ``` 95 | 96 | com.amazonaws 97 | dynamodb-streams-kinesis-adapter 98 | 1.5.1 99 | 100 | ``` 101 | 102 | ## Building From Source 103 | 104 | Once you check out the code from GitHub, you can build it using Maven: `mvn clean install` 105 | 106 | [adapter-issues]: https://github.com/awslabs/dynamodb-streams-kinesis-adapter/issues 107 | [adapter-maven]: http://mvnrepository.com/artifact/com.amazonaws/dynamodb-streams-kinesis-adapter 108 | [dynamodb-details]: https://aws.amazon.com/dynamodb 109 | [dynamodb-forum]: https://developer.amazonwebservices.com/connect/forum.jspa?forumID=131 110 | [docs-adapter]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.KCLAdapter.html 111 | [docs-dynamodb-streams]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html 112 | [docs-kcl]: https://github.com/awslabs/amazon-kinesis-client 113 | [docs-signup]: https://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-setup.html 114 | [kcl-configuration]: https://github.com/awslabs/amazon-kinesis-client/blob/master/src/main/java/com/amazonaws/services/kinesis/clientlibrary/lib/worker/KinesisClientLibConfiguration.java 115 | -------------------------------------------------------------------------------- /build.properties: -------------------------------------------------------------------------------- 1 | source.. = src/main/java 2 | output.. = bin/ 3 | 4 | bin.includes = LICENSE.txt,\ 5 | NOTICE.txt,\ 6 | META-INF/,\ 7 | . 8 | 9 | jre.compilation.profile = JavaSE-1.7 10 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/AdapterRequestCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter; 7 | 8 | import java.util.Deque; 9 | import java.util.HashMap; 10 | import java.util.LinkedList; 11 | 12 | import com.amazonaws.AmazonWebServiceRequest; 13 | 14 | /** 15 | * Cache for mapping Kinesis requests to DynamoDB Streams requests. Evicts oldest cache entry if adding a new entry exceeds the cache capacity. 16 | */ 17 | public class AdapterRequestCache { 18 | /** 19 | * Map for request lookup. 20 | */ 21 | private final HashMap cacheMap = new HashMap(); 22 | /** 23 | * Deque for evicting old cache entries. 24 | */ 25 | private final Deque evictQueue = new LinkedList(); 26 | /** 27 | * Capacity for the cache. 28 | */ 29 | private final int capacity; 30 | 31 | public AdapterRequestCache(int capacity) { 32 | if (capacity <= 0) { 33 | throw new IllegalArgumentException("Capacity must be a positive number"); 34 | } 35 | this.capacity = capacity; 36 | } 37 | 38 | /** 39 | * Adds an entry to the cache. 40 | * 41 | * @param request Kinesis request 42 | * @param requestAdapter DynamoDB adapter client wrapper for the Kinesis request 43 | */ 44 | public synchronized void addEntry(AmazonWebServiceRequest request, AmazonWebServiceRequest requestAdapter) { 45 | if (null == request || null == requestAdapter) { 46 | throw new IllegalArgumentException("Request and adapter request must not be null"); 47 | } 48 | if (evictQueue.size() == capacity) { 49 | Integer evicted = evictQueue.removeLast(); 50 | cacheMap.remove(evicted); 51 | } 52 | evictQueue.addFirst(System.identityHashCode(request)); 53 | cacheMap.put(System.identityHashCode(request), requestAdapter); 54 | } 55 | 56 | /** 57 | * Gets the actual DynamoDB Streams request made for a Kinesis request. 58 | * 59 | * @param request Kinesis request 60 | * @return actual DynamoDB Streams request made for the associated Kinesis request 61 | */ 62 | public synchronized AmazonWebServiceRequest getEntry(AmazonWebServiceRequest request) { 63 | if (null == request) { 64 | throw new IllegalArgumentException("Request must not be null"); 65 | } 66 | return cacheMap.get(System.identityHashCode(request)); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/DynamoDBStreamsShardConsumerFactory.java: -------------------------------------------------------------------------------- 1 | package com.amazonaws.services.dynamodbv2.streamsadapter; 2 | 3 | 4 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.ICheckpoint; 5 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessor; 6 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.IShardConsumer; 7 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.IShardConsumerFactory; 8 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.KinesisClientLibConfiguration; 9 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.KinesisClientLibLeaseCoordinator; 10 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.RecordProcessorCheckpointer; 11 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.ShardInfo; 12 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.ShardSyncStrategy; 13 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.ShardSyncer; 14 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.StreamConfig; 15 | import com.amazonaws.services.kinesis.leases.impl.LeaseCleanupManager; 16 | import com.amazonaws.services.kinesis.metrics.interfaces.IMetricsFactory; 17 | 18 | import java.util.Optional; 19 | import java.util.concurrent.ExecutorService; 20 | 21 | public class DynamoDBStreamsShardConsumerFactory implements IShardConsumerFactory { 22 | 23 | 24 | public DynamoDBStreamsShardConsumerFactory(){}; 25 | 26 | @Override 27 | public IShardConsumer createShardConsumer(ShardInfo shardInfo, 28 | StreamConfig streamConfig, 29 | ICheckpoint checkpointTracker, 30 | IRecordProcessor recordProcessor, 31 | RecordProcessorCheckpointer recordProcessorCheckpointer, 32 | KinesisClientLibLeaseCoordinator leaseCoordinator, 33 | long parentShardPollIntervalMillis, 34 | boolean cleanupLeasesUponShardCompletion, 35 | ExecutorService executorService, 36 | IMetricsFactory metricsFactory, 37 | long taskBackoffTimeMillis, 38 | boolean skipShardSyncAtWorkerInitializationIfLeasesExist, 39 | Optional retryGetRecordsInSeconds, 40 | Optional maxGetRecordsThreadPool, 41 | KinesisClientLibConfiguration config, ShardSyncer shardSyncer, ShardSyncStrategy shardSyncStrategy, 42 | LeaseCleanupManager leaseCleanupManager) { 43 | return new DynamoDBStreamsShardConsumer(shardInfo, 44 | streamConfig, 45 | checkpointTracker, 46 | recordProcessor, 47 | recordProcessorCheckpointer, 48 | leaseCoordinator, 49 | parentShardPollIntervalMillis, 50 | cleanupLeasesUponShardCompletion, 51 | executorService, 52 | metricsFactory, 53 | taskBackoffTimeMillis, 54 | skipShardSyncAtWorkerInitializationIfLeasesExist, 55 | new DynamoDBStreamsDataFetcher(streamConfig.getStreamProxy(), shardInfo), 56 | retryGetRecordsInSeconds, 57 | maxGetRecordsThreadPool, 58 | config, shardSyncer, shardSyncStrategy, 59 | leaseCleanupManager); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/StreamsDeterministicShuffleShardSyncLeaderDecider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter; 7 | 8 | import java.util.Collections; 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Random; 12 | import java.util.Set; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.ScheduledExecutorService; 15 | import java.util.concurrent.TimeUnit; 16 | import java.util.concurrent.locks.ReadWriteLock; 17 | import java.util.concurrent.locks.ReentrantReadWriteLock; 18 | import java.util.stream.Collectors; 19 | 20 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.KinesisClientLibConfiguration; 21 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.LeaderDecider; 22 | import com.amazonaws.services.kinesis.leases.exceptions.DependencyException; 23 | import com.amazonaws.services.kinesis.leases.exceptions.InvalidStateException; 24 | import com.amazonaws.services.kinesis.leases.exceptions.ProvisionedThroughputException; 25 | import com.amazonaws.services.kinesis.leases.impl.KinesisClientLease; 26 | import com.amazonaws.services.kinesis.leases.interfaces.ILeaseManager; 27 | import com.amazonaws.util.CollectionUtils; 28 | import org.apache.commons.logging.Log; 29 | import org.apache.commons.logging.LogFactory; 30 | 31 | /** 32 | * An implementation of the {@code LeaderDecider} to elect leader(s) based on workerId. 33 | * Leases are shuffled using a predetermined constant seed so that lease ordering is 34 | * preserved across workers. 35 | * This reduces the probability of choosing the leader workers co-located on the same 36 | * host in case workerId starts with a common string (e.g. IP Address). 37 | * Hence if a host has 3 workers, IPADDRESS_Worker1, IPADDRESS_Worker2, and IPADDRESS_Worker3, 38 | * we don't end up choosing all 3 for shard sync as a result of natural ordering of Strings. 39 | * This ensures redundancy for shard-sync during host failures. 40 | */ 41 | 42 | public class StreamsDeterministicShuffleShardSyncLeaderDecider implements LeaderDecider { 43 | private static final Log LOG = LogFactory.getLog(StreamsDeterministicShuffleShardSyncLeaderDecider.class); 44 | 45 | // Fixed seed so that the shuffle order is preserved across workers 46 | static final int DETERMINISTIC_SHUFFLE_SEED = 1947; 47 | static final int PERIODIC_SHARD_SYNC_MAX_WORKERS_DEFAULT = 10; //Default for DynamoDB Streams consumers. 48 | 49 | private static final int LEADER_DECIDER_THREAD_COUNT = 1; 50 | private static final long ELECTION_INITIAL_DELAY_MILLIS = 60 * 1000; 51 | private static final long ELECTION_SCHEDULING_INTERVAL = 5 * 60 * 1000; 52 | private static final int AWAIT_TERMINATION_MILLIS = 5000; 53 | 54 | private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 55 | 56 | private final KinesisClientLibConfiguration config; 57 | private final ILeaseManager leaseManager; 58 | private final int numPeriodicShardSyncWorkers; 59 | private final ScheduledExecutorService leaderElectionThreadPool; 60 | 61 | private volatile Set leaders; 62 | 63 | /** 64 | * Create an instance using the default periodic shard sync worker count. 65 | * This constructor is package-private to ensure external users use the public 66 | * constructor(s) and are thereby aware of the number of periodic shard sync workers 67 | * they want in their application. 68 | * @param config KinesisClientLibConfiguration instance 69 | * @param leaseManager LeaseManager instance. 70 | */ 71 | StreamsDeterministicShuffleShardSyncLeaderDecider(KinesisClientLibConfiguration config, ILeaseManager leaseManager) { 72 | this(config, leaseManager, PERIODIC_SHARD_SYNC_MAX_WORKERS_DEFAULT); 73 | } 74 | 75 | /** 76 | * Create an instance overriding the default periodic shard sync worker count. 77 | * This is intentionally public for use by consumers to build KCL workers 78 | * using KCL Worker Builder directly instead of using StreamsWorkerFactory. 79 | * @param config KinesisClientLibConfiguration instance 80 | * @param leaseManager LeaseManager instance. 81 | * @param numPeriodicShardSyncWorkers Max number of periodic shard sync workers. 82 | */ 83 | public StreamsDeterministicShuffleShardSyncLeaderDecider(KinesisClientLibConfiguration config, 84 | ILeaseManager leaseManager, int numPeriodicShardSyncWorkers) { 85 | this(config, leaseManager, Executors.newScheduledThreadPool(LEADER_DECIDER_THREAD_COUNT), numPeriodicShardSyncWorkers); 86 | } 87 | 88 | // package-private for use in unit tests. 89 | StreamsDeterministicShuffleShardSyncLeaderDecider(KinesisClientLibConfiguration config, 90 | ILeaseManager leaseManager, ScheduledExecutorService leaderElectionThreadPool, 91 | int numPeriodicShardSyncWorkers) { 92 | this.config = config; 93 | this.leaseManager = leaseManager; 94 | this.leaderElectionThreadPool = leaderElectionThreadPool; 95 | this.numPeriodicShardSyncWorkers = numPeriodicShardSyncWorkers; 96 | } 97 | 98 | /* 99 | * Shuffles the leases deterministically and elects numPeriodicShardSyncWorkers number of workers 100 | * as leaders (workers that will perform shard sync). 101 | */ 102 | private void electLeaders() { 103 | try { 104 | LOG.debug("Started leader election: " + System.currentTimeMillis()); 105 | List leases = leaseManager.listLeases(); 106 | List uniqueHosts = leases.stream().map(KinesisClientLease::getLeaseOwner) 107 | .filter(owner -> owner != null).distinct().sorted().collect(Collectors.toList()); 108 | 109 | Collections.shuffle(uniqueHosts, new Random(DETERMINISTIC_SHUFFLE_SEED)); 110 | int numShardSyncWorkers = Math.min(uniqueHosts.size(), numPeriodicShardSyncWorkers); 111 | // In case value is currently being read, we wait for reading to complete before updating the variable. 112 | // This is to prevent any ConcurrentModificationException exceptions. 113 | readWriteLock.writeLock().lock(); 114 | leaders = new HashSet<>(uniqueHosts.subList(0, numShardSyncWorkers)); 115 | LOG.info("Elected leaders: " + String.join(", ", leaders)); 116 | LOG.debug("Completed leader election: " + System.currentTimeMillis()); 117 | } catch (DependencyException | InvalidStateException | ProvisionedThroughputException e) { 118 | LOG.error("Exception occurred while trying to fetch all leases for leader election", e); 119 | } catch (Throwable t) { 120 | LOG.error("Unknown exception during leader election.", t); 121 | } finally { 122 | readWriteLock.writeLock().unlock(); 123 | } 124 | } 125 | 126 | // Utility methods to ensure we acquire a readLock in case an election has completed 127 | // and some thread is trying to update leaders variable. 128 | private boolean leadersNullOrEmpty() { 129 | try { 130 | readWriteLock.readLock().lock(); 131 | return CollectionUtils.isNullOrEmpty(leaders); 132 | } finally { 133 | readWriteLock.readLock().unlock(); 134 | } 135 | } 136 | 137 | private boolean isLeaderForShardSync(String workerId) { 138 | try { 139 | readWriteLock.readLock().lock(); 140 | // If leaders is still null or empty fall back to this host being a "leader". 141 | // This ensures that a brief unavailability or throttling on the leases table does not cause a stall. 142 | return CollectionUtils.isNullOrEmpty(leaders) || (!CollectionUtils.isNullOrEmpty(leaders) && leaders.contains(workerId)); 143 | } finally { 144 | readWriteLock.readLock().unlock(); 145 | } 146 | } 147 | 148 | @Override 149 | public synchronized Boolean isLeader(String workerId) { 150 | // if no leaders yet, synchronously get leaders. This will happen at first Shard Sync. 151 | if (leadersNullOrEmpty()) { 152 | electLeaders(); 153 | // start a scheduled executor that will periodically update leaders. 154 | // The first run will be after a minute. 155 | // We don't need jitter since it is scheduled with a fixed delay and time taken to scan leases 156 | // will be different at different times and on different hosts/workers. 157 | leaderElectionThreadPool.scheduleWithFixedDelay(this::electLeaders, ELECTION_INITIAL_DELAY_MILLIS , 158 | ELECTION_SCHEDULING_INTERVAL, TimeUnit.MILLISECONDS); 159 | } 160 | return isLeaderForShardSync(workerId); 161 | } 162 | 163 | @Override 164 | public synchronized void shutdown() { 165 | try { 166 | leaderElectionThreadPool.shutdown(); 167 | if (leaderElectionThreadPool.awaitTermination(AWAIT_TERMINATION_MILLIS, TimeUnit.MILLISECONDS)) { 168 | LOG.info("Successfully stopped leader election on the worker"); 169 | } else { 170 | leaderElectionThreadPool.shutdownNow(); 171 | LOG.info(String.format("Stopped leader election thread after awaiting termination for %d milliseconds", 172 | AWAIT_TERMINATION_MILLIS)); 173 | } 174 | 175 | } catch (InterruptedException e) { 176 | LOG.debug("Encountered InterruptedException while awaiting leader election threadPool termination"); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/StreamsLeaseCleanupValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter; 7 | 8 | import com.amazonaws.services.kinesis.clientlibrary.exceptions.internal.KinesisClientLibIOException; 9 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.LeaseCleanupValidator; 10 | import com.amazonaws.services.kinesis.leases.impl.KinesisClientLease; 11 | import org.apache.commons.logging.Log; 12 | import org.apache.commons.logging.LogFactory; 13 | 14 | import java.util.Set; 15 | 16 | /** 17 | * Decides if a lease is eligible for cleanup. 18 | */ 19 | public class StreamsLeaseCleanupValidator implements LeaseCleanupValidator { 20 | 21 | private static final Log LOG = LogFactory.getLog(StreamsLeaseCleanupValidator.class); 22 | 23 | /** 24 | * @param lease Candidate shard we are considering for deletion. 25 | * @param currentKinesisShardIds List of leases currently held by the worker. 26 | * @return true if neither the shard (corresponding to the lease), nor its parents are present in 27 | * currentKinesisShardIds 28 | * @throws KinesisClientLibIOException Thrown if currentKinesisShardIds contains a parent shard but not the child 29 | * shard (we are evaluating for deletion). 30 | */ 31 | @Override 32 | public boolean isCandidateForCleanup(KinesisClientLease lease, Set currentKinesisShardIds) throws KinesisClientLibIOException { 33 | boolean isCandidateForCleanup = true; 34 | 35 | if (currentKinesisShardIds.contains(lease.getLeaseKey())) { 36 | isCandidateForCleanup = false; 37 | } else { 38 | LOG.info("Found lease for non-existent shard: " + lease.getLeaseKey() + ". Checking its parent shards"); 39 | Set parentShardIds = lease.getParentShardIds(); 40 | for (String parentShardId : parentShardIds) { 41 | 42 | // Throw an exception if the parent shard exists (but the child does not). 43 | // This may be a (rare) race condition between fetching the shard list and Kinesis expiring shards. 44 | if (currentKinesisShardIds.contains(parentShardId)) { 45 | String message = 46 | "Parent shard " + parentShardId + " exists but not the child shard " 47 | + lease.getLeaseKey(); 48 | LOG.info(message); 49 | throw new KinesisClientLibIOException(message); 50 | } 51 | } 52 | } 53 | 54 | return isCandidateForCleanup; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/StreamsMultiLangDaemon.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter; 7 | 8 | import java.io.IOException; 9 | import java.util.concurrent.ExecutionException; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Future; 12 | 13 | import org.apache.commons.logging.Log; 14 | import org.apache.commons.logging.LogFactory; 15 | 16 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.Worker; 17 | import com.amazonaws.services.kinesis.multilang.MultiLangDaemon; 18 | import com.amazonaws.services.kinesis.multilang.MultiLangDaemonConfig; 19 | 20 | /** 21 | * Main app that launches the worker that runs the multi-language record processor. 22 | * Requires a properties file containing configuration for this daemon and the KCL. 23 | *

24 | * This version extends the KCL's MultiLangDaemon to use DynamoDB Streams instead of 25 | * Kinesis. 26 | */ 27 | public class StreamsMultiLangDaemon { 28 | 29 | private static final Log LOG = LogFactory.getLog(StreamsMultiLangDaemon.class); 30 | 31 | /** 32 | * @param args Accepts a single argument, that argument is a properties file which provides KCL configuration as 33 | * well as the name of an executable. 34 | */ 35 | public static void main(String[] args) { 36 | 37 | if (args.length == 0) { 38 | MultiLangDaemon.printUsage(System.err, "You must provide a properties file"); 39 | System.exit(1); 40 | } 41 | MultiLangDaemonConfig config = null; 42 | try { 43 | config = new MultiLangDaemonConfig(args[0]); 44 | } catch (IOException | IllegalArgumentException e) { 45 | MultiLangDaemon.printUsage(System.err, "You must provide a valid properties file"); 46 | System.exit(1); 47 | } 48 | 49 | final ExecutorService executorService = config.getExecutorService(); 50 | final Worker worker = StreamsWorkerFactory.createDynamoDbStreamsWorker(config.getRecordProcessorFactory(), config.getKinesisClientLibConfiguration(), executorService); 51 | 52 | // Daemon 53 | final MultiLangDaemon daemon = new MultiLangDaemon(worker); 54 | 55 | final Future future = executorService.submit(daemon); 56 | try { 57 | System.exit(future.get()); 58 | } catch (InterruptedException | ExecutionException e) { 59 | LOG.error("Encountered an error while running daemon", e); 60 | } 61 | System.exit(1); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/StreamsRecordProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import org.apache.commons.logging.Log; 12 | import org.apache.commons.logging.LogFactory; 13 | 14 | import com.amazonaws.services.dynamodbv2.streamsadapter.model.RecordAdapter; 15 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.IRecordProcessorCheckpointer; 16 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessor; 17 | import com.amazonaws.services.kinesis.clientlibrary.types.InitializationInput; 18 | import com.amazonaws.services.kinesis.clientlibrary.types.ProcessRecordsInput; 19 | import com.amazonaws.services.kinesis.clientlibrary.types.ShutdownInput; 20 | import com.amazonaws.services.kinesis.model.Record; 21 | 22 | /** 23 | * This record processor is intended for use with the DynamoDB Streams Adapter for the 24 | * Amazon Kinesis Client Library (KCL). It will retrieve the underlying Streams records 25 | * from the KCL adapter in order to simplify record processing tasks. 26 | */ 27 | public abstract class StreamsRecordProcessor implements IRecordProcessor { 28 | 29 | private static final Log LOG = LogFactory.getLog(StreamsRecordProcessor.class); 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public abstract void initialize(InitializationInput initializationInput); 35 | 36 | public void processRecords(ProcessRecordsInput processRecordsInput) { 37 | final List streamsRecords = new ArrayList(); 38 | if (processRecordsInput.getRecords() == null) { 39 | LOG.warn("ProcessRecordsInput's list of Records was null. Skipping."); 40 | return; 41 | } 42 | for (Record record : processRecordsInput.getRecords()) { 43 | if (record instanceof RecordAdapter) { 44 | streamsRecords.add(((RecordAdapter) record).getInternalObject()); 45 | } else { 46 | // This record processor is not being used with the 47 | // DynamoDB Streams Adapter for Amazon Kinesis Client 48 | // Library, so we cannot retrieve any Streams records. 49 | throw new IllegalArgumentException("Record is not an instance of RecordAdapter"); 50 | } 51 | } 52 | processStreamsRecords(streamsRecords, processRecordsInput.getCheckpointer()); 53 | } 54 | 55 | /** 56 | * Process data records. The Amazon Kinesis Client Library will invoke this method to deliver data records to the 57 | * application. 58 | * Upon fail over, the new instance will get records with sequence number > checkpoint position 59 | * for each partition key. 60 | * 61 | * @param records Data records to be processed 62 | * @param checkpointer RecordProcessor should use this instance to checkpoint their progress. 63 | */ 64 | public abstract void processStreamsRecords(List records, IRecordProcessorCheckpointer checkpointer); 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public abstract void shutdown(ShutdownInput shutdownInput); 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/exceptions/UnableToReadMoreRecordsException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.exceptions; 7 | 8 | /** 9 | * This exception is thrown when records have been trimmed and the user has specified to not continue processing. 10 | */ 11 | public class UnableToReadMoreRecordsException extends RuntimeException { 12 | 13 | private static final long serialVersionUID = 7280447889113524297L; 14 | 15 | public UnableToReadMoreRecordsException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/DescribeStreamRequestAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import com.amazonaws.services.dynamodbv2.model.DescribeStreamRequest; 9 | 10 | /** 11 | * Container for the parameters to the DescribeStream operation. 12 | */ 13 | public class DescribeStreamRequestAdapter extends DescribeStreamRequest { 14 | 15 | private com.amazonaws.services.kinesis.model.DescribeStreamRequest internalRequest; 16 | 17 | /** 18 | * Constructs a new request using an Amazon Kinesis object. 19 | * 20 | * @param request Instance of Amazon Kinesis DescribeStreamRequest 21 | */ 22 | public DescribeStreamRequestAdapter(com.amazonaws.services.kinesis.model.DescribeStreamRequest request) { 23 | internalRequest = request; 24 | } 25 | 26 | /** 27 | * @return The shard ID of the shard to start with for the stream description. 28 | */ 29 | @Override 30 | public String getExclusiveStartShardId() { 31 | return internalRequest.getExclusiveStartShardId(); 32 | } 33 | 34 | /** 35 | * @param exclusiveStartShardId The shard ID of the shard to start with for the stream description. 36 | */ 37 | @Override 38 | public void setExclusiveStartShardId(String exclusiveStartShardId) { 39 | internalRequest.setExclusiveStartShardId(exclusiveStartShardId); 40 | } 41 | 42 | /** 43 | * @param exclusiveStartShardId The shard ID of the shard to start with for the stream description. 44 | * @return A reference to this updated object so that method calls can be chained together. 45 | */ 46 | @Override 47 | public DescribeStreamRequest withExclusiveStartShardId(String exclusiveStartShardId) { 48 | internalRequest.setExclusiveStartShardId(exclusiveStartShardId); 49 | return this; 50 | } 51 | 52 | /** 53 | * @return The maximum number of shards to return. 54 | */ 55 | @Override 56 | public Integer getLimit() { 57 | return internalRequest.getLimit(); 58 | } 59 | 60 | /** 61 | * @param limit The maximum number of shards to return. 62 | */ 63 | @Override 64 | public void setLimit(Integer limit) { 65 | internalRequest.setLimit(limit); 66 | } 67 | 68 | /** 69 | * @param limit The maximum number of shards to return. 70 | * @return A reference to this updated object so that method calls can be chained together. 71 | */ 72 | @Override 73 | public DescribeStreamRequest withLimit(Integer limit) { 74 | internalRequest.setLimit(limit); 75 | return this; 76 | } 77 | 78 | /** 79 | * @return The ARN of the stream to describe. 80 | */ 81 | @Override 82 | public String getStreamArn() { 83 | return internalRequest.getStreamName(); 84 | } 85 | 86 | /** 87 | * @param streamArn The ARN of the stream to describe. 88 | */ 89 | @Override 90 | public void setStreamArn(String streamArn) { 91 | internalRequest.setStreamName(streamArn); 92 | } 93 | 94 | /** 95 | * @param streamArn The ARN of the stream to describe. 96 | * @return A reference to this updated object so that method calls can be chained together. 97 | */ 98 | @Override 99 | public DescribeStreamRequest withStreamArn(String streamArn) { 100 | internalRequest.setStreamName(streamArn); 101 | return this; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/DescribeStreamResultAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import com.amazonaws.services.kinesis.model.DescribeStreamResult; 9 | import com.amazonaws.services.kinesis.model.StreamDescription; 10 | 11 | /** 12 | * Represents the output of a DescribeStream operation. 13 | */ 14 | public class DescribeStreamResultAdapter extends DescribeStreamResult { 15 | 16 | private StreamDescription streamDescription; 17 | 18 | /** 19 | * Constructs a new result using a DynamoDBStreams object. 20 | * 21 | * @param result Instance of DynamoDBStreams DescribeStreamResult 22 | */ 23 | public DescribeStreamResultAdapter(com.amazonaws.services.dynamodbv2.model.DescribeStreamResult result) { 24 | streamDescription = new StreamDescriptionAdapter(result.getStreamDescription()); 25 | } 26 | 27 | /** 28 | * @return Contains the current status of the stream, the stream ARN, an array of 29 | * shard objects that comprise the stream, and states whether there are 30 | * more shards available. 31 | */ 32 | @Override 33 | public StreamDescription getStreamDescription() { 34 | return streamDescription; 35 | } 36 | 37 | @Override 38 | public void setStreamDescription(StreamDescription streamDescription) { 39 | throw new UnsupportedOperationException(); 40 | } 41 | 42 | @Override 43 | public DescribeStreamResult withStreamDescription(StreamDescription streamDescription) { 44 | throw new UnsupportedOperationException(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/GetRecordsRequestAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import com.amazonaws.services.dynamodbv2.model.GetRecordsRequest; 9 | 10 | /** 11 | * Container for the parameters to the GetRecords operation. 12 | */ 13 | public class GetRecordsRequestAdapter extends GetRecordsRequest { 14 | 15 | private com.amazonaws.services.kinesis.model.GetRecordsRequest internalRequest; 16 | 17 | /** 18 | * Constructs a new request from an Amazon Kinesis object. 19 | * 20 | * @param request Instance of AmazonKinesis GetRecordsReqest 21 | */ 22 | public GetRecordsRequestAdapter(com.amazonaws.services.kinesis.model.GetRecordsRequest request) { 23 | internalRequest = request; 24 | } 25 | 26 | /** 27 | * @return The maximum number of records to return. 28 | */ 29 | @Override 30 | public Integer getLimit() { 31 | return internalRequest.getLimit(); 32 | } 33 | 34 | /** 35 | * @param limit The maximum number of records to return. 36 | */ 37 | @Override 38 | public void setLimit(Integer limit) { 39 | internalRequest.setLimit(limit); 40 | } 41 | 42 | /** 43 | * @param limit The maximum number of records to return. 44 | * @return A reference to this updated object so that method calls can be chained together. 45 | */ 46 | @Override 47 | public GetRecordsRequest withLimit(Integer limit) { 48 | internalRequest.setLimit(limit); 49 | return this; 50 | } 51 | 52 | /** 53 | * @return The position in the shard from which you want to start sequentially 54 | * reading data records. 55 | */ 56 | @Override 57 | public String getShardIterator() { 58 | return internalRequest.getShardIterator(); 59 | } 60 | 61 | /** 62 | * @param shardIterator The position in the shard from which you want to start sequentially 63 | * reading data records. 64 | */ 65 | @Override 66 | public void setShardIterator(String shardIterator) { 67 | internalRequest.setShardIterator(shardIterator); 68 | } 69 | 70 | /** 71 | * @param shardIterator The position in the shard from which you want to start sequentially 72 | * reading data records. 73 | * @return A reference to this updated object so that method calls can be chained together. 74 | */ 75 | @Override 76 | public GetRecordsRequest withShardIterator(String shardIterator) { 77 | internalRequest.setShardIterator(shardIterator); 78 | return this; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/GetRecordsResultAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import com.amazonaws.services.kinesis.model.GetRecordsResult; 9 | import com.amazonaws.services.kinesis.model.Record; 10 | 11 | /** 12 | * Represents the output of a GetRecords operation. 13 | */ 14 | public class GetRecordsResultAdapter extends GetRecordsResult { 15 | 16 | private com.amazonaws.services.dynamodbv2.model.GetRecordsResult internalResult; 17 | 18 | private java.util.List records; 19 | 20 | /** 21 | * Constructs a new result using a DynamoDBStreams object. 22 | * 23 | * @param result Instance of DynamoDBStreams GetRecordsResult 24 | */ 25 | public GetRecordsResultAdapter(com.amazonaws.services.dynamodbv2.model.GetRecordsResult result) { 26 | this(result, true); 27 | } 28 | 29 | /** 30 | * Constructs a new result using a DynamoDBStreams object. 31 | * 32 | * @param result Instance of DynamoDBStreams GetRecordsResult 33 | * @param generateRecordDataBytes Whether or not RecordAdapters should generate the ByteBuffer returned by getData(). KCL 34 | * uses the bytes returned by getData to generate throughput metrics. If these metrics are not needed then 35 | * choosing to not generate this data results in memory and CPU savings. 36 | */ 37 | public GetRecordsResultAdapter(com.amazonaws.services.dynamodbv2.model.GetRecordsResult result, boolean generateRecordDataBytes) { 38 | internalResult = result; 39 | records = new java.util.ArrayList(); 40 | if (result.getRecords() != null) { 41 | for (com.amazonaws.services.dynamodbv2.model.Record record : result.getRecords()) { 42 | records.add(new RecordAdapter(record, generateRecordDataBytes)); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * @return The data records retrieved from the shard 49 | */ 50 | @Override 51 | public java.util.List getRecords() { 52 | return records; 53 | } 54 | 55 | @Override 56 | public void setRecords(java.util.Collection records) { 57 | throw new UnsupportedOperationException(); 58 | } 59 | 60 | @Override 61 | public GetRecordsResult withRecords(Record... records) { 62 | throw new UnsupportedOperationException(); 63 | } 64 | 65 | @Override 66 | public GetRecordsResult withRecords(java.util.Collection records) { 67 | throw new UnsupportedOperationException(); 68 | } 69 | 70 | /** 71 | * @return The next position in the shard from which to start sequentially 72 | * reading data records. If set to null, the shard has been 73 | * closed and the requested iterator will not return any more data. 74 | */ 75 | @Override 76 | public String getNextShardIterator() { 77 | return internalResult.getNextShardIterator(); 78 | } 79 | 80 | @Override 81 | public void setNextShardIterator(String nextShardIterator) { 82 | throw new UnsupportedOperationException(); 83 | } 84 | 85 | @Override 86 | public GetRecordsResult withNextShardIterator(String nextShardIterator) { 87 | throw new UnsupportedOperationException(); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/GetShardIteratorRequestAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import com.amazonaws.services.dynamodbv2.model.GetShardIteratorRequest; 9 | import com.amazonaws.services.dynamodbv2.model.ShardIteratorType; 10 | 11 | /** 12 | * Container for the parameters to the GetShardIterator operation. 13 | */ 14 | public class GetShardIteratorRequestAdapter extends GetShardIteratorRequest { 15 | // Evaluate each ShardIteratorType toString() only once. 16 | private static final String SHARD_ITERATOR_TYPE_DYNAMODB_AT_SEQUENCE_NUMBER = ShardIteratorType.AT_SEQUENCE_NUMBER.toString(); 17 | private static final String SHARD_ITERATOR_TYPE_DYNAMODB_AFTER_SEQUENCE_NUMBER = ShardIteratorType.AFTER_SEQUENCE_NUMBER.toString(); 18 | private static final String SHARD_ITERATOR_TYPE_DYNAMODB_LATEST = ShardIteratorType.LATEST.toString(); 19 | private static final String SHARD_ITERATOR_TYPE_DYNAMODB_TRIM_HORIZON = ShardIteratorType.TRIM_HORIZON.toString(); 20 | 21 | private static final String SHARD_ITERATOR_TYPE_KINESIS_AFTER_SEQUENCE_NUMBER = com.amazonaws.services.kinesis.model.ShardIteratorType.AFTER_SEQUENCE_NUMBER.toString(); 22 | private static final String SHARD_ITERATOR_TYPE_KINESIS_AT_SEQUENCE_NUMBER = com.amazonaws.services.kinesis.model.ShardIteratorType.AT_SEQUENCE_NUMBER.toString(); 23 | private static final String SHARD_ITERATOR_TYPE_KINESIS_LATEST = com.amazonaws.services.kinesis.model.ShardIteratorType.LATEST.toString(); 24 | private static final String SHARD_ITERATOR_TYPE_KINESIS_TRIM_HORIZON = com.amazonaws.services.kinesis.model.ShardIteratorType.TRIM_HORIZON.toString(); 25 | 26 | private com.amazonaws.services.kinesis.model.GetShardIteratorRequest internalRequest; 27 | 28 | /** 29 | * Constructs a new request using an AmazonKinesis object. 30 | * 31 | * @param request Instance of AmazonKinesis GetShardIteratorRequest 32 | */ 33 | public GetShardIteratorRequestAdapter(com.amazonaws.services.kinesis.model.GetShardIteratorRequest request) { 34 | internalRequest = request; 35 | } 36 | 37 | /** 38 | * @return The ARN of the stream. 39 | */ 40 | @Override 41 | public String getStreamArn() { 42 | return internalRequest.getStreamName(); 43 | } 44 | 45 | /** 46 | * @param streamArn The ARN of the stream. 47 | */ 48 | @Override 49 | public void setStreamArn(String streamArn) { 50 | internalRequest.setStreamName(streamArn); 51 | } 52 | 53 | /** 54 | * @param streamArn The ARN of the stream. 55 | * @return Returns a reference to this object so that method calls can be chained together. 56 | */ 57 | @Override 58 | public GetShardIteratorRequest withStreamArn(String streamArn) { 59 | internalRequest.withStreamName(streamArn); 60 | return this; 61 | } 62 | 63 | /** 64 | * @return The shard ID of the shard to get the iterator for. 65 | */ 66 | @Override 67 | public String getShardId() { 68 | return internalRequest.getShardId(); 69 | } 70 | 71 | /** 72 | * @param shardId The shard ID of the shard to get the iterator for. 73 | */ 74 | @Override 75 | public void setShardId(String shardId) { 76 | internalRequest.setShardId(shardId); 77 | } 78 | 79 | /** 80 | * @param shardId The shard ID of the shard to get the iterator for. 81 | * @return Returns a reference to this object so that method calls can be chained together. 82 | */ 83 | @Override 84 | public GetShardIteratorRequest withShardId(String shardId) { 85 | internalRequest.withShardId(shardId); 86 | return this; 87 | } 88 | 89 | /** 90 | * @return The sequence number of the data record in the shard from which to 91 | * start reading from. 92 | */ 93 | @Override 94 | public String getSequenceNumber() { 95 | return internalRequest.getStartingSequenceNumber(); 96 | } 97 | 98 | /** 99 | * @param sequenceNumber The sequence number of the data record in the shard from which to 100 | * start reading from. 101 | */ 102 | @Override 103 | public void setSequenceNumber(String sequenceNumber) { 104 | internalRequest.setStartingSequenceNumber(sequenceNumber); 105 | } 106 | 107 | /** 108 | * @param sequenceNumber The sequence number of the data record in the shard from which to 109 | * start reading from. 110 | * @return Returns a reference to this object so that method calls can be chained together. 111 | */ 112 | @Override 113 | public GetShardIteratorRequest withSequenceNumber(String sequenceNumber) { 114 | internalRequest.withStartingSequenceNumber(sequenceNumber); 115 | return this; 116 | } 117 | 118 | /** 119 | * @return Determines how the shard iterator is used to start reading data 120 | * records from the shard. 121 | */ 122 | @Override 123 | public String getShardIteratorType() { 124 | return internalRequest.getShardIteratorType(); 125 | } 126 | 127 | /** 128 | * @param shardIteratorType Determines how the shard iterator is used to start reading data 129 | * records from the shard. 130 | */ 131 | @Override 132 | public void setShardIteratorType(String shardIteratorType) { 133 | if (SHARD_ITERATOR_TYPE_DYNAMODB_TRIM_HORIZON.equals(shardIteratorType)) { 134 | internalRequest.setShardIteratorType(SHARD_ITERATOR_TYPE_KINESIS_TRIM_HORIZON); 135 | } else if (SHARD_ITERATOR_TYPE_DYNAMODB_LATEST.equals(shardIteratorType)) { 136 | internalRequest.setShardIteratorType(SHARD_ITERATOR_TYPE_KINESIS_LATEST); 137 | } else if (SHARD_ITERATOR_TYPE_DYNAMODB_AT_SEQUENCE_NUMBER.equals(shardIteratorType)) { 138 | internalRequest.setShardIteratorType(SHARD_ITERATOR_TYPE_KINESIS_AT_SEQUENCE_NUMBER); 139 | } else if (SHARD_ITERATOR_TYPE_DYNAMODB_AFTER_SEQUENCE_NUMBER.equals(shardIteratorType)) { 140 | internalRequest.setShardIteratorType(SHARD_ITERATOR_TYPE_KINESIS_AFTER_SEQUENCE_NUMBER); 141 | } else { 142 | throw new IllegalArgumentException("Unsupported ShardIteratorType: " + shardIteratorType); 143 | } 144 | } 145 | 146 | /** 147 | * @param shardIteratorType Determines how the shard iterator is used to start reading data 148 | * records from the shard. 149 | */ 150 | @Override 151 | public void setShardIteratorType(ShardIteratorType shardIteratorType) { 152 | setShardIteratorType(shardIteratorType.toString()); 153 | } 154 | 155 | /** 156 | * @param shardIteratorType Determines how the shard iterator is used to start reading data 157 | * records from the shard. 158 | * @return Returns a reference to this object so that method calls can be chained together. 159 | */ 160 | @Override 161 | public GetShardIteratorRequest withShardIteratorType(String shardIteratorType) { 162 | setShardIteratorType(shardIteratorType); 163 | return this; 164 | } 165 | 166 | /** 167 | * @param shardIteratorType Determines how the shard iterator is used to start reading data 168 | * records from the shard. 169 | * @return Returns a reference to this object so that method calls can be chained together. 170 | */ 171 | @Override 172 | public GetShardIteratorRequest withShardIteratorType(ShardIteratorType shardIteratorType) { 173 | setShardIteratorType(shardIteratorType); 174 | return this; 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/GetShardIteratorResultAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import com.amazonaws.services.kinesis.model.GetShardIteratorResult; 9 | 10 | /** 11 | * Represents the output of a GetShardIterator operation. 12 | */ 13 | public class GetShardIteratorResultAdapter extends GetShardIteratorResult { 14 | 15 | private com.amazonaws.services.dynamodbv2.model.GetShardIteratorResult internalResult; 16 | 17 | /** 18 | * Constructs a new result using a DynamoDBStreams object. 19 | * 20 | * @param result Instance of DynamoDBStreams GetShardIteratorResult 21 | */ 22 | public GetShardIteratorResultAdapter(com.amazonaws.services.dynamodbv2.model.GetShardIteratorResult result) { 23 | internalResult = result; 24 | } 25 | 26 | /** 27 | * @return The position in the shard from which to start reading data records 28 | * sequentially. 29 | */ 30 | @Override 31 | public String getShardIterator() { 32 | return internalResult.getShardIterator(); 33 | } 34 | 35 | @Override 36 | public void setShardIterator(String shardIterator) { 37 | throw new UnsupportedOperationException(); 38 | } 39 | 40 | @Override 41 | public GetShardIteratorResult withShardIterator(String shardIterator) { 42 | throw new UnsupportedOperationException(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/ListStreamsRequestAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import com.amazonaws.services.dynamodbv2.model.ListStreamsRequest; 9 | 10 | /** 11 | * Container for the parameters to the ListStreams operation. 12 | */ 13 | public class ListStreamsRequestAdapter extends ListStreamsRequest { 14 | 15 | private com.amazonaws.services.kinesis.model.ListStreamsRequest internalRequest; 16 | 17 | /** 18 | * Constructs a new request using an AmazonKinesis object. 19 | * 20 | * @param request Instance of AmazonKinesis ListStreamsRequest 21 | */ 22 | public ListStreamsRequestAdapter(com.amazonaws.services.kinesis.model.ListStreamsRequest request) { 23 | internalRequest = request; 24 | } 25 | 26 | /** 27 | * @return The name of the stream to start the list with. 28 | */ 29 | @Override 30 | public String getExclusiveStartStreamArn() { 31 | return internalRequest.getExclusiveStartStreamName(); 32 | } 33 | 34 | /** 35 | * @param exclusiveStartStreamArn The name of the stream to start the list with. 36 | */ 37 | @Override 38 | public void setExclusiveStartStreamArn(String exclusiveStartStreamArn) { 39 | internalRequest.setExclusiveStartStreamName(exclusiveStartStreamArn); 40 | } 41 | 42 | /** 43 | * @param exclusiveStartStreamArn The name of the stream to start the list with. 44 | * @return A reference to this updated object so that method calls can be chained together. 45 | */ 46 | @Override 47 | public ListStreamsRequest withExclusiveStartStreamArn(String exclusiveStartStreamArn) { 48 | this.setExclusiveStartStreamArn(exclusiveStartStreamArn); 49 | return this; 50 | } 51 | 52 | /** 53 | * @return The maximum number of streams to list. 54 | */ 55 | @Override 56 | public Integer getLimit() { 57 | return internalRequest.getLimit(); 58 | } 59 | 60 | /** 61 | * @param limit The maximum number of streams to list. 62 | */ 63 | @Override 64 | public void setLimit(Integer limit) { 65 | internalRequest.setLimit(limit); 66 | } 67 | 68 | /** 69 | * @param limit The maximum number of streams to list. 70 | * @return A reference to this updated object so that method calls can be chained together. 71 | */ 72 | @Override 73 | public ListStreamsRequest withLimit(Integer limit) { 74 | this.setLimit(limit); 75 | return this; 76 | } 77 | 78 | // Not supported by the underlying Kinesis class 79 | @Override 80 | public String getTableName() { 81 | throw new UnsupportedOperationException(); 82 | } 83 | 84 | // Not supported by the underlying Kinesis class 85 | @Override 86 | public void setTableName(String tableName) { 87 | throw new UnsupportedOperationException(); 88 | } 89 | 90 | // Not supported by the underlying Kinesis class 91 | @Override 92 | public ListStreamsRequest withTableName(String tableName) { 93 | throw new UnsupportedOperationException(); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/ListStreamsResultAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import com.amazonaws.services.kinesis.model.ListStreamsResult; 12 | 13 | /** 14 | * Represents the output of a ListStreams operation. 15 | */ 16 | public class ListStreamsResultAdapter extends ListStreamsResult { 17 | 18 | private com.amazonaws.services.dynamodbv2.model.ListStreamsResult internalResult; 19 | 20 | /** 21 | * Constructs a new result using a DynamoDBStreams object. 22 | * 23 | * @param result Instance of DynamoDBStreams ListStreamsResult 24 | */ 25 | public ListStreamsResultAdapter(com.amazonaws.services.dynamodbv2.model.ListStreamsResult result) { 26 | internalResult = result; 27 | } 28 | 29 | /** 30 | * The names of the streams that are associated with the AWS account 31 | * making the request. 32 | */ 33 | @Override 34 | public List getStreamNames() { 35 | List streams = internalResult.getStreams(); 36 | List streamArns = new ArrayList<>(streams.size()); 37 | for (com.amazonaws.services.dynamodbv2.model.Stream stream : streams) { 38 | streamArns.add(stream.getStreamArn()); 39 | } 40 | return streamArns; 41 | } 42 | 43 | @Override 44 | public void setStreamNames(java.util.Collection streamNames) { 45 | throw new UnsupportedOperationException(); 46 | } 47 | 48 | @Override 49 | public ListStreamsResult withStreamNames(java.util.Collection streamNames) { 50 | throw new UnsupportedOperationException(); 51 | } 52 | 53 | @Override 54 | public ListStreamsResult withStreamNames(String... streamNames) { 55 | throw new UnsupportedOperationException(); 56 | } 57 | 58 | /** 59 | * @return If true, there are more streams available to list. 60 | */ 61 | @Override 62 | public Boolean isHasMoreStreams() { 63 | return internalResult.getLastEvaluatedStreamArn() != null; 64 | } 65 | 66 | /** 67 | * @return If true, there are more streams available to list. 68 | */ 69 | @Override 70 | public Boolean getHasMoreStreams() { 71 | return internalResult.getLastEvaluatedStreamArn() != null; 72 | } 73 | 74 | @Override 75 | public void setHasMoreStreams(Boolean hasMoreStreams) { 76 | throw new UnsupportedOperationException(); 77 | } 78 | 79 | @Override 80 | public ListStreamsResult withHasMoreStreams(Boolean hasMoreStreams) { 81 | throw new UnsupportedOperationException(); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/RecordAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | import java.nio.charset.Charset; 11 | import java.util.Date; 12 | 13 | import org.apache.commons.logging.Log; 14 | import org.apache.commons.logging.LogFactory; 15 | 16 | import com.amazonaws.services.kinesis.model.Record; 17 | import com.fasterxml.jackson.core.JsonProcessingException; 18 | import com.fasterxml.jackson.databind.ObjectMapper; 19 | 20 | /** 21 | * A single update notification of a DynamoDB Stream, adapted for use 22 | * with the Amazon Kinesis model. 23 | *

24 | * This class is designed to be used in a single thread only. 25 | */ 26 | public class RecordAdapter extends Record { 27 | 28 | private static Log LOG = LogFactory.getLog(RecordAdapter.class); 29 | 30 | public static final Charset defaultCharset = Charset.forName("UTF-8"); 31 | 32 | private static final ObjectMapper MAPPER = new RecordObjectMapper(); 33 | 34 | private final com.amazonaws.services.dynamodbv2.model.Record internalRecord; 35 | 36 | private ByteBuffer data; 37 | 38 | private boolean generateDataBytes; 39 | 40 | /** 41 | * Constructs a new record using a DynamoDBStreams object. 42 | * 43 | * @param record Instance of DynamoDBStreams Record 44 | */ 45 | public RecordAdapter(com.amazonaws.services.dynamodbv2.model.Record record) { 46 | this(record, true); 47 | } 48 | 49 | /** 50 | * Constructor for internal use 51 | * 52 | * @param record 53 | * @param generateDataBytes Whether or not to generate the ByteBuffer returned by getData(). KCL 54 | * uses the bytes returned by getData to generate throughput metrics. If these metrics are not needed then 55 | * choosing to not generate this data results in memory and CPU savings. If this value is true then 56 | * the data will be generated. If false, getData() will return an empty ByteBuffer. 57 | * @throws IOException 58 | */ 59 | RecordAdapter(com.amazonaws.services.dynamodbv2.model.Record record, boolean generateDataBytes) { 60 | internalRecord = record; 61 | this.generateDataBytes = generateDataBytes; 62 | } 63 | 64 | /** 65 | * @return The underlying DynamoDBStreams object 66 | */ 67 | public com.amazonaws.services.dynamodbv2.model.Record getInternalObject() { 68 | return internalRecord; 69 | } 70 | 71 | /** 72 | * @return The unique identifier for the record in the DynamoDB stream. 73 | */ 74 | @Override 75 | public String getSequenceNumber() { 76 | return internalRecord.getDynamodb().getSequenceNumber(); 77 | } 78 | 79 | @Override 80 | public void setSequenceNumber(String sequenceNumber) { 81 | throw new UnsupportedOperationException(); 82 | } 83 | 84 | @Override 85 | public Record withSequenceNumber(String sequenceNumber) { 86 | throw new UnsupportedOperationException(); 87 | } 88 | 89 | /** 90 | * This method returns JSON serialized {@link Record} object. However, This is not the best to use the object 91 | * It is recommended to get an object using {@link #getInternalObject()} and cast appropriately. 92 | * 93 | * @return JSON serialization of {@link Record} object. JSON contains only non-null 94 | * fields of {@link com.amazonaws.services.dynamodbv2.model.Record}. It returns null if serialization fails. 95 | */ 96 | @Override 97 | public ByteBuffer getData() { 98 | if (data == null) { 99 | if (generateDataBytes) { 100 | try { 101 | data = ByteBuffer.wrap(MAPPER.writeValueAsString(internalRecord).getBytes(defaultCharset)); 102 | } catch (JsonProcessingException e) { 103 | final String errorMessage = "Failed to serialize stream record to JSON"; 104 | LOG.error(errorMessage, e); 105 | throw new RuntimeException(errorMessage, e); 106 | } 107 | } else { 108 | data = ByteBuffer.wrap(new byte[0]); 109 | } 110 | } 111 | return data; 112 | } 113 | 114 | @Override 115 | public void setData(ByteBuffer data) { 116 | throw new UnsupportedOperationException(); 117 | } 118 | 119 | @Override 120 | public Record withData(ByteBuffer data) { 121 | throw new UnsupportedOperationException(); 122 | } 123 | 124 | /** 125 | * Jackson ObjectMapper requires a valid return value for serialization. 126 | */ 127 | @Override 128 | public String getPartitionKey() { 129 | return null; 130 | } 131 | 132 | @Override 133 | public void setPartitionKey(String partitionKey) { 134 | throw new UnsupportedOperationException(); 135 | } 136 | 137 | @Override 138 | public Record withPartitionKey(String partitionKey) { 139 | throw new UnsupportedOperationException(); 140 | } 141 | 142 | @Override 143 | public Date getApproximateArrivalTimestamp() { 144 | return internalRecord.getDynamodb().getApproximateCreationDateTime(); 145 | } 146 | 147 | @Override 148 | public void setApproximateArrivalTimestamp(Date approximateArrivalTimestamp) { 149 | internalRecord.getDynamodb().setApproximateCreationDateTime(approximateArrivalTimestamp); 150 | } 151 | 152 | @Override 153 | public Record withApproximateArrivalTimestamp(Date approximateArrivalTimestamp) { 154 | setApproximateArrivalTimestamp(approximateArrivalTimestamp); 155 | return this; 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/ShardAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import com.amazonaws.services.kinesis.model.HashKeyRange; 9 | import com.amazonaws.services.kinesis.model.SequenceNumberRange; 10 | import com.amazonaws.services.kinesis.model.Shard; 11 | 12 | /** 13 | * A uniquely identified group of data records in a DynamoDB 14 | * stream. 15 | */ 16 | public class ShardAdapter extends Shard { 17 | 18 | private com.amazonaws.services.dynamodbv2.model.Shard internalShard; 19 | 20 | /** 21 | * Constructs a new shard description using a DynamoDBStreams object. 22 | * 23 | * @param shard Instance of DynamoDBStreams Shard 24 | */ 25 | public ShardAdapter(com.amazonaws.services.dynamodbv2.model.Shard shard) { 26 | internalShard = shard; 27 | } 28 | 29 | /** 30 | * @return The unique identifier of the shard within the DynamoDB stream. 31 | */ 32 | @Override 33 | public String getShardId() { 34 | return internalShard.getShardId(); 35 | } 36 | 37 | @Override 38 | public void setShardId(String shardId) { 39 | throw new UnsupportedOperationException(); 40 | } 41 | 42 | @Override 43 | public Shard withShardId(String shardId) { 44 | throw new UnsupportedOperationException(); 45 | } 46 | 47 | /** 48 | * @return The shard Id of the shard's parent. 49 | */ 50 | @Override 51 | public String getParentShardId() { 52 | return internalShard.getParentShardId(); 53 | } 54 | 55 | @Override 56 | public void setParentShardId(String parentShardId) { 57 | throw new UnsupportedOperationException(); 58 | } 59 | 60 | @Override 61 | public Shard withParentShardId(String parentShardId) { 62 | throw new UnsupportedOperationException(); 63 | } 64 | 65 | /** 66 | * The Kinesis model provides an adjacent parent shard ID in the event of 67 | * a parent shard merge. Since DynamoDB Streams does not support merge, this 68 | * always returns null. 69 | * 70 | * @return The shard Id of the shard adjacent to the shard's parent. 71 | */ 72 | @Override 73 | public String getAdjacentParentShardId() { 74 | return null; 75 | } 76 | 77 | @Override 78 | public void setAdjacentParentShardId(String adjacentParentShardId) { 79 | throw new UnsupportedOperationException(); 80 | } 81 | 82 | @Override 83 | public Shard withAdjacentParentShardId(String adjacentParentShardId) { 84 | throw new UnsupportedOperationException(); 85 | } 86 | 87 | /** 88 | * The underlying DynamoDB Streams model does not expose hash key range. To 89 | * ensure compatibility with the Kinesis Client Library, this method 90 | * returns dummy values. 91 | * 92 | * @return The range of possible hash key values for the shard. 93 | */ 94 | @Override 95 | public HashKeyRange getHashKeyRange() { 96 | HashKeyRange hashKeyRange = new HashKeyRange(); 97 | hashKeyRange.setStartingHashKey(java.math.BigInteger.ZERO.toString()); 98 | hashKeyRange.setEndingHashKey(java.math.BigInteger.ONE.toString()); 99 | return hashKeyRange; 100 | } 101 | 102 | @Override 103 | public void setHashKeyRange(HashKeyRange hashKeyRange) { 104 | throw new UnsupportedOperationException(); 105 | } 106 | 107 | @Override 108 | public Shard withHashKeyRange(HashKeyRange hashKeyRange) { 109 | throw new UnsupportedOperationException(); 110 | } 111 | 112 | /** 113 | * @return The range of possible sequence numbers for the shard. 114 | */ 115 | @Override 116 | public SequenceNumberRange getSequenceNumberRange() { 117 | SequenceNumberRange sequenceNumberRange = new SequenceNumberRange(); 118 | sequenceNumberRange.setStartingSequenceNumber(internalShard.getSequenceNumberRange().getStartingSequenceNumber()); 119 | sequenceNumberRange.setEndingSequenceNumber(internalShard.getSequenceNumberRange().getEndingSequenceNumber()); 120 | return sequenceNumberRange; 121 | } 122 | 123 | @Override 124 | public void setSequenceNumberRange(SequenceNumberRange sequenceNumberRange) { 125 | throw new UnsupportedOperationException(); 126 | } 127 | 128 | @Override 129 | public Shard withSequenceNumberRange(SequenceNumberRange sequenceNumberRange) { 130 | throw new UnsupportedOperationException(); 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/StreamDescriptionAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import com.amazonaws.services.kinesis.model.Shard; 12 | import com.amazonaws.services.kinesis.model.StreamDescription; 13 | import com.amazonaws.services.kinesis.model.StreamStatus; 14 | 15 | /** 16 | * Container for all information describing a single DynamoDB Stream. 17 | */ 18 | public class StreamDescriptionAdapter extends StreamDescription { 19 | // Evaluate each StreamStatus.toString() only once 20 | private static final String STREAM_STATUS_DYNAMODB_DISABLED = com.amazonaws.services.dynamodbv2.model.StreamStatus.DISABLED.toString(); 21 | private static final String STREAM_STATUS_DYNAMODB_DISABLING = com.amazonaws.services.dynamodbv2.model.StreamStatus.DISABLING.toString(); 22 | private static final String STREAM_STATUS_DYNAMODB_ENABLED = com.amazonaws.services.dynamodbv2.model.StreamStatus.ENABLED.toString(); 23 | private static final String STREAM_STATUS_DYNAMODB_ENABLING = com.amazonaws.services.dynamodbv2.model.StreamStatus.ENABLING.toString(); 24 | private static final String STREAM_STATUS_KINESIS_ACTIVE = StreamStatus.ACTIVE.toString(); 25 | private static final String STREAM_STATUS_KINESIS_CREATING = StreamStatus.CREATING.toString(); 26 | 27 | private final com.amazonaws.services.dynamodbv2.model.StreamDescription internalDescription; 28 | 29 | private final List shards; 30 | 31 | /** 32 | * Constructs a new description using a DynamoDBStreams object. 33 | * 34 | * @param streamDescription Instance of DynamoDBStreams StreamDescription 35 | */ 36 | public StreamDescriptionAdapter(com.amazonaws.services.dynamodbv2.model.StreamDescription streamDescription) { 37 | internalDescription = streamDescription; 38 | shards = new ArrayList(); 39 | for (com.amazonaws.services.dynamodbv2.model.Shard shard : streamDescription.getShards()) { 40 | shards.add(new ShardAdapter(shard)); 41 | } 42 | } 43 | 44 | /** 45 | * @return The underlying DynamoDBStreams object 46 | */ 47 | public com.amazonaws.services.dynamodbv2.model.StreamDescription getInternalObject() { 48 | return internalDescription; 49 | } 50 | 51 | /** 52 | * @return The name of the stream being described. 53 | */ 54 | @Override 55 | public String getStreamName() { 56 | return internalDescription.getStreamArn(); 57 | } 58 | 59 | @Override 60 | public void setStreamName(String streamName) { 61 | throw new UnsupportedOperationException(); 62 | } 63 | 64 | @Override 65 | public StreamDescription withStreamName(String streamName) { 66 | throw new UnsupportedOperationException(); 67 | } 68 | 69 | /** 70 | * @return The Amazon Resource Name (ARN) for the stream being described. 71 | */ 72 | @Override 73 | public String getStreamARN() { 74 | return internalDescription.getStreamArn(); 75 | } 76 | 77 | @Override 78 | public void setStreamARN(String streamARN) { 79 | throw new UnsupportedOperationException(); 80 | } 81 | 82 | @Override 83 | public StreamDescription withStreamARN(String streamARN) { 84 | throw new UnsupportedOperationException(); 85 | } 86 | 87 | /** 88 | * @return The current status of the stream being described. 89 | */ 90 | @Override 91 | public String getStreamStatus() { 92 | String status = internalDescription.getStreamStatus(); 93 | if (STREAM_STATUS_DYNAMODB_ENABLED.equals(status)) { 94 | status = STREAM_STATUS_KINESIS_ACTIVE; 95 | } else if (STREAM_STATUS_DYNAMODB_ENABLING.equals(status)) { 96 | status = STREAM_STATUS_KINESIS_CREATING; 97 | } else if (STREAM_STATUS_DYNAMODB_DISABLED.equals(status)) { 98 | // streams are valid for 24hrs after disabling and 99 | // will continue to support read operations 100 | status = STREAM_STATUS_KINESIS_ACTIVE; 101 | } else if (STREAM_STATUS_DYNAMODB_DISABLING.equals(status)) { 102 | status = STREAM_STATUS_KINESIS_ACTIVE; 103 | } else { 104 | throw new UnsupportedOperationException("Unsupported StreamStatus: " + status); 105 | } 106 | return status; 107 | } 108 | 109 | @Override 110 | public void setStreamStatus(String streamStatus) { 111 | throw new UnsupportedOperationException(); 112 | } 113 | 114 | @Override 115 | public StreamDescription withStreamStatus(String streamStatus) { 116 | throw new UnsupportedOperationException(); 117 | } 118 | 119 | @Override 120 | public void setStreamStatus(StreamStatus streamStatus) { 121 | throw new UnsupportedOperationException(); 122 | } 123 | 124 | @Override 125 | public StreamDescription withStreamStatus(StreamStatus streamStatus) { 126 | throw new UnsupportedOperationException(); 127 | } 128 | 129 | /** 130 | * @return The shards that comprise the stream. 131 | */ 132 | @Override 133 | public List getShards() { 134 | return shards; 135 | } 136 | 137 | @Override 138 | public void setShards(java.util.Collection shards) { 139 | throw new UnsupportedOperationException(); 140 | } 141 | 142 | @Override 143 | public StreamDescription withShards(Shard... shards) { 144 | throw new UnsupportedOperationException(); 145 | } 146 | 147 | @Override 148 | public StreamDescription withShards(java.util.Collection shards) { 149 | throw new UnsupportedOperationException(); 150 | } 151 | 152 | /** 153 | * @return If true there are more shards in the stream 154 | * available to describe. 155 | */ 156 | @Override 157 | public Boolean isHasMoreShards() { 158 | return internalDescription.getLastEvaluatedShardId() != null; 159 | } 160 | 161 | /** 162 | * @return If true there are more shards in the stream 163 | * available to describe. 164 | */ 165 | @Override 166 | public Boolean getHasMoreShards() { 167 | return internalDescription.getLastEvaluatedShardId() != null; 168 | } 169 | 170 | @Override 171 | public void setHasMoreShards(Boolean hasMoreShards) { 172 | throw new UnsupportedOperationException(); 173 | } 174 | 175 | @Override 176 | public StreamDescription withHasMoreShards(Boolean hasMoreShards) { 177 | throw new UnsupportedOperationException(); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/utils/Sleeper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Amazon Software License (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/asl/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazonaws.services.dynamodbv2.streamsadapter.utils; 16 | 17 | /** 18 | * Interface to implement mechanisms to inject specified delay 19 | * in processing similar to Thread.sleep(long millis). 20 | */ 21 | public interface Sleeper { 22 | 23 | void sleep(long intervalInMillis); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/services/dynamodbv2/streamsadapter/utils/ThreadSleeper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Amazon Software License (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/asl/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazonaws.services.dynamodbv2.streamsadapter.utils; 16 | 17 | import org.apache.commons.logging.Log; 18 | import org.apache.commons.logging.LogFactory; 19 | 20 | /** 21 | * Simple abstraction over Thread.sleep() to allow unit testing of backoff mechanisms. 22 | */ 23 | public class ThreadSleeper implements Sleeper { 24 | private static final Log LOG = LogFactory.getLog(ThreadSleeper.class); 25 | 26 | @Override public void sleep(long intervalInMillis) { 27 | try { 28 | Thread.sleep(intervalInMillis); 29 | } catch (InterruptedException ie) { 30 | LOG.debug("ThreadSleeper sleep was interrupted ", ie); 31 | Thread.currentThread().interrupt(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/AdapterRequestCacheTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertNull; 10 | import static org.junit.Assert.assertTrue; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.Random; 15 | 16 | import org.junit.Test; 17 | 18 | import com.amazonaws.AmazonWebServiceRequest; 19 | import com.amazonaws.services.dynamodbv2.streamsadapter.model.GetRecordsRequestAdapter; 20 | import com.amazonaws.services.kinesis.model.GetRecordsRequest; 21 | 22 | public class AdapterRequestCacheTests { 23 | 24 | private static final int CACHE_SIZE = 50; 25 | 26 | @Test 27 | public void testSanityConstructor() { 28 | AdapterRequestCache requestCache = new AdapterRequestCache(CACHE_SIZE); 29 | assertTrue(requestCache instanceof AdapterRequestCache); 30 | } 31 | 32 | @Test(expected = IllegalArgumentException.class) 33 | public void testZeroCapacity() { 34 | new AdapterRequestCache(0); 35 | } 36 | 37 | @Test(expected = IllegalArgumentException.class) 38 | public void testNegativeCapacity() { 39 | Random r = new Random(); 40 | int positiveNumber = r.nextInt(Integer.MAX_VALUE - 1) + 1; 41 | new AdapterRequestCache(-1 * positiveNumber); 42 | } 43 | 44 | @Test(expected = IllegalArgumentException.class) 45 | public void testNullRequestAdd() { 46 | AdapterRequestCache requestCache = new AdapterRequestCache(CACHE_SIZE); 47 | requestCache.addEntry(null, new GetRecordsRequestAdapter(new GetRecordsRequest())); 48 | } 49 | 50 | @Test(expected = IllegalArgumentException.class) 51 | public void testNullRequestAdapterAdd() { 52 | AdapterRequestCache requestCache = new AdapterRequestCache(CACHE_SIZE); 53 | requestCache.addEntry(new GetRecordsRequest(), null); 54 | } 55 | 56 | @Test(expected = IllegalArgumentException.class) 57 | public void testNullRequestGet() { 58 | AdapterRequestCache requestCache = new AdapterRequestCache(CACHE_SIZE); 59 | requestCache.getEntry(null); 60 | } 61 | 62 | @Test 63 | public void testCacheSanity() { 64 | AdapterRequestCache requestCache = new AdapterRequestCache(CACHE_SIZE); 65 | GetRecordsRequest request = new GetRecordsRequest(); 66 | GetRecordsRequestAdapter requestAdapter = new GetRecordsRequestAdapter(request); 67 | requestCache.addEntry(request, requestAdapter); 68 | AmazonWebServiceRequest entry = requestCache.getEntry(request); 69 | assertEquals(System.identityHashCode(requestAdapter), System.identityHashCode(entry)); 70 | } 71 | 72 | @Test 73 | public void testEviction() { 74 | AdapterRequestCache requestCache = new AdapterRequestCache(CACHE_SIZE); 75 | int testLength = 2 * CACHE_SIZE; 76 | List requests = new ArrayList(testLength); 77 | List requestAdapters = new ArrayList(testLength); 78 | for (int i = 0; i < testLength; i++) { 79 | // Construct requests 80 | GetRecordsRequest request = new GetRecordsRequest(); 81 | GetRecordsRequestAdapter requestAdapter = new GetRecordsRequestAdapter(request); 82 | // Store references to request for validation 83 | requests.add(request); 84 | requestAdapters.add(requestAdapter); 85 | // Add entry to the request cache 86 | requestCache.addEntry(request, requestAdapter); 87 | 88 | // Verify request cache 89 | for (int j = 0; j <= i; j++) { 90 | AmazonWebServiceRequest expected = requestAdapters.get(j); 91 | AmazonWebServiceRequest actual = requestCache.getEntry(requests.get(j)); 92 | if (j <= i - CACHE_SIZE) { 93 | assertNull(actual); 94 | } else { 95 | assertEquals(System.identityHashCode(expected), System.identityHashCode(actual)); 96 | } 97 | } 98 | 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/DynamoDBStreamsPeriodicShardSyncManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.amazonaws.services.dynamodbv2.streamsadapter; 2 | 3 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.IPeriodicShardSyncManager; 4 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.LeaderDecider; 5 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.ShardSyncTask; 6 | import com.amazonaws.services.kinesis.clientlibrary.proxies.IKinesisProxy; 7 | import com.amazonaws.services.kinesis.leases.impl.KinesisClientLease; 8 | import com.amazonaws.services.kinesis.leases.interfaces.ILeaseManager; 9 | import com.amazonaws.services.kinesis.metrics.impl.NullMetricsFactory; 10 | import com.amazonaws.services.kinesis.metrics.interfaces.IMetricsFactory; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.mockito.Mock; 16 | import org.mockito.runners.MockitoJUnitRunner; 17 | 18 | import java.util.Collections; 19 | import static org.mockito.Mockito.when; 20 | 21 | @RunWith(MockitoJUnitRunner.class) 22 | public class DynamoDBStreamsPeriodicShardSyncManagerTest { 23 | private static final String WORKER_ID = "workerId"; 24 | public static final long LEASES_RECOVERY_AUDITOR_EXECUTION_FREQUENCY_MILLIS = 2 * 60 * 1000L; 25 | public static final int LEASES_RECOVERY_AUDITOR_INCONSISTENCY_CONFIDENCE_THRESHOLD = 3; 26 | 27 | /** Manager for PERIODIC shard sync strategy */ 28 | private IPeriodicShardSyncManager periodicShardSyncManager; 29 | 30 | /** Manager for SHARD_END shard sync strategy */ 31 | private IPeriodicShardSyncManager auditorPeriodicShardSyncManager; 32 | 33 | @Mock 34 | private LeaderDecider leaderDecider; 35 | @Mock 36 | private ShardSyncTask shardSyncTask; 37 | @Mock 38 | private ILeaseManager leaseManager; 39 | @Mock 40 | private IKinesisProxy kinesisProxy; 41 | 42 | private IMetricsFactory metricsFactory = new NullMetricsFactory(); 43 | 44 | @Before 45 | public void setup() { 46 | periodicShardSyncManager = new DynamoDBStreamsPeriodicShardSyncManager(WORKER_ID, leaderDecider, shardSyncTask, 47 | metricsFactory, leaseManager, kinesisProxy, false, LEASES_RECOVERY_AUDITOR_EXECUTION_FREQUENCY_MILLIS, 48 | LEASES_RECOVERY_AUDITOR_INCONSISTENCY_CONFIDENCE_THRESHOLD); 49 | auditorPeriodicShardSyncManager = new DynamoDBStreamsPeriodicShardSyncManager(WORKER_ID, leaderDecider, shardSyncTask, 50 | metricsFactory, leaseManager, kinesisProxy, true, LEASES_RECOVERY_AUDITOR_EXECUTION_FREQUENCY_MILLIS, 51 | LEASES_RECOVERY_AUDITOR_INCONSISTENCY_CONFIDENCE_THRESHOLD); 52 | } 53 | 54 | @Test 55 | public void testIfShardSyncIsInitiatedWhenNoLeasesArePassed() throws Exception { 56 | when(leaseManager.listLeases()).thenReturn(null); 57 | Assert.assertTrue(((DynamoDBStreamsPeriodicShardSyncManager) periodicShardSyncManager).checkForShardSync().shouldDoShardSync()); 58 | Assert.assertTrue(((DynamoDBStreamsPeriodicShardSyncManager) auditorPeriodicShardSyncManager).checkForShardSync().shouldDoShardSync()); 59 | } 60 | 61 | @Test 62 | public void testIfShardSyncIsInitiatedWhenEmptyLeasesArePassed() throws Exception { 63 | when(leaseManager.listLeases()).thenReturn(Collections.emptyList()); 64 | Assert.assertTrue(((DynamoDBStreamsPeriodicShardSyncManager) periodicShardSyncManager).checkForShardSync().shouldDoShardSync()); 65 | Assert.assertTrue(((DynamoDBStreamsPeriodicShardSyncManager) auditorPeriodicShardSyncManager).checkForShardSync().shouldDoShardSync()); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/PeriodicShardSyncTestBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Random; 11 | 12 | import com.amazonaws.services.kinesis.clientlibrary.types.ExtendedSequenceNumber; 13 | import com.amazonaws.services.kinesis.leases.impl.KinesisClientLease; 14 | 15 | public class PeriodicShardSyncTestBase { 16 | 17 | private static final String LEASE_KEY = "lease_key"; 18 | private static final String LEASE_OWNER = "lease_owner"; 19 | 20 | protected List getLeases(int count, boolean duplicateLeaseOwner, boolean activeLeases) { 21 | List leases = new ArrayList<>(); 22 | for (int i=0;i leaseManager; 42 | 43 | @Mock 44 | private ScheduledExecutorService scheduledExecutorService; 45 | 46 | private int numShardSyncWorkers; 47 | 48 | @Before 49 | public void setup() { 50 | numShardSyncWorkers = PERIODIC_SHARD_SYNC_MAX_WORKERS_DEFAULT; 51 | leaderDecider = new StreamsDeterministicShuffleShardSyncLeaderDecider(config, leaseManager, scheduledExecutorService, numShardSyncWorkers); 52 | config = new KinesisClientLibConfiguration("Test", null, null, null); 53 | } 54 | 55 | @Test 56 | public void testLeaderElectionWithNullLeases() { 57 | boolean isLeader = leaderDecider.isLeader(WORKER_ID); 58 | assertTrue("IsLeader should return true if leaders is null", isLeader); 59 | } 60 | 61 | @Test 62 | public void testLeaderElectionWithEmptyLeases() throws Exception{ 63 | when(leaseManager.listLeases()).thenReturn(new ArrayList<>()); 64 | boolean isLeader = leaderDecider.isLeader(WORKER_ID); 65 | assertTrue("IsLeader should return true if no leases are returned", isLeader); 66 | } 67 | 68 | @Test 69 | public void testElectedLeadersAsPerExpectedShufflingOrder() throws Exception { 70 | List leases = getLeases(5, false /* duplicateLeaseOwner */, true /* activeLeases */); 71 | when(leaseManager.listLeases()).thenReturn(leases); 72 | Set expectedLeaders = getExpectedLeaders(leases); 73 | for (String leader : expectedLeaders) { 74 | assertTrue(leaderDecider.isLeader(leader)); 75 | } 76 | for (KinesisClientLease lease : leases) { 77 | if (!expectedLeaders.contains(lease.getLeaseOwner())) { 78 | assertFalse(leaderDecider.isLeader(lease.getLeaseOwner())); 79 | } 80 | } 81 | } 82 | 83 | @Test 84 | public void testElectedLeadersAsPerExpectedShufflingOrderWhenUniqueWorkersLessThanMaxLeaders() { 85 | this.numShardSyncWorkers = 5; // More than number of unique lease owners 86 | leaderDecider = new StreamsDeterministicShuffleShardSyncLeaderDecider(config, leaseManager, scheduledExecutorService, numShardSyncWorkers); 87 | List leases = getLeases(3, false /* duplicateLeaseOwner */, true /* activeLeases */); 88 | Set expectedLeaders = getExpectedLeaders(leases); 89 | // All lease owners should be present in expected leaders set, and they should all be leaders. 90 | for (KinesisClientLease lease : leases) { 91 | assertTrue(leaderDecider.isLeader(lease.getLeaseOwner())); 92 | assertTrue(expectedLeaders.contains(lease.getLeaseOwner())); 93 | } 94 | } 95 | 96 | private Set getExpectedLeaders(List leases) { 97 | List uniqueHosts = leases.stream().filter(lease -> lease.getLeaseOwner() != null) 98 | .map(KinesisClientLease::getLeaseOwner).distinct().sorted().collect(Collectors.toList()); 99 | 100 | Collections.shuffle(uniqueHosts, new Random(DETERMINISTIC_SHUFFLE_SEED)); 101 | int numWorkers = Math.min(uniqueHosts.size(), this.numShardSyncWorkers); 102 | return new HashSet<>(uniqueHosts.subList(0, numWorkers)); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/StreamsRecordProcessorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | 17 | import com.amazonaws.services.dynamodbv2.model.Record; 18 | import com.amazonaws.services.dynamodbv2.model.StreamRecord; 19 | import com.amazonaws.services.dynamodbv2.streamsadapter.model.RecordAdapter; 20 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.IRecordProcessorCheckpointer; 21 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessor; 22 | import com.amazonaws.services.kinesis.clientlibrary.types.InitializationInput; 23 | import com.amazonaws.services.kinesis.clientlibrary.types.ProcessRecordsInput; 24 | import com.amazonaws.services.kinesis.clientlibrary.types.ShutdownInput; 25 | 26 | public class StreamsRecordProcessorTest { 27 | 28 | private Record testRecord; 29 | private IRecordProcessor recordProcessor; 30 | 31 | @Before 32 | public void setUp() { 33 | recordProcessor = new SimpleStreamsRecordProcessor(); 34 | } 35 | 36 | @Test 37 | public void testProcessRecordsSuccess() throws IOException { 38 | testRecord = new Record().withDynamodb(new StreamRecord()).withEventID("test").withEventName("MODIFY"); 39 | RecordAdapter adapter = new RecordAdapter(testRecord); 40 | List recordList = new ArrayList(); 41 | recordList.add(adapter); 42 | recordProcessor.processRecords(new ProcessRecordsInput().withRecords(recordList)); 43 | } 44 | 45 | @Test(expected = IllegalArgumentException.class) 46 | public void testProcessRecordsFail() { 47 | List recordList = new ArrayList(); 48 | recordList.add(new com.amazonaws.services.kinesis.model.Record()); 49 | recordProcessor.processRecords(new ProcessRecordsInput().withRecords(recordList)); 50 | } 51 | 52 | private class SimpleStreamsRecordProcessor extends StreamsRecordProcessor { 53 | 54 | @Override 55 | public void initialize(InitializationInput initializationInput) { 56 | 57 | } 58 | 59 | @Override 60 | public void processStreamsRecords(List records, IRecordProcessorCheckpointer checkpointer) { 61 | assertEquals(testRecord, records.get(0)); 62 | } 63 | 64 | @Override 65 | public void shutdown(ShutdownInput shutdownInput) { 66 | 67 | } 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/functionals/CorrectnessTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.functionals; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.ScheduledExecutorService; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import org.apache.commons.logging.Log; 15 | import org.apache.commons.logging.LogFactory; 16 | import org.junit.Before; 17 | import org.junit.Test; 18 | 19 | import com.amazonaws.services.dynamodbv2.model.QueryResult; 20 | import com.amazonaws.services.dynamodbv2.model.ScanResult; 21 | import com.amazonaws.services.dynamodbv2.streamsadapter.util.ReplicatingRecordProcessor; 22 | import com.amazonaws.services.dynamodbv2.streamsadapter.util.TestUtil; 23 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.InitialPositionInStream; 24 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.KinesisClientLibConfiguration; 25 | 26 | /** 27 | * This test runs KCL with the DynamoDB Streams Kinesis Adapter using a single partition table and no shard lineage in 28 | * an embedded DynamoDB local instance. A series of operations are performed on the source table and then replicated on 29 | * a destination table using the records received by the IRecordProcessor implementation. Finally, a scan/query is 30 | * performed on both tables to assert that the expected records are replicated. 31 | */ 32 | public class CorrectnessTest extends FunctionalTestBase { 33 | private static final Log LOG = LogFactory.getLog(CorrectnessTest.class); 34 | 35 | private int numItemsInSrcTable = 0; 36 | 37 | private static int NUM_INITIAL_ITEMS = 2; 38 | 39 | @Before 40 | public void setup() { 41 | super.setup(); 42 | insertAndUpdateItems(NUM_INITIAL_ITEMS); 43 | } 44 | 45 | @Test 46 | public void trimHorizonTest() throws Exception { 47 | LOG.info("Starting single shard KCL integration test with TRIM_HORIZON."); 48 | 49 | KinesisClientLibConfiguration workerConfig = 50 | new KinesisClientLibConfiguration(leaseTable, streamId, credentials, KCL_WORKER_ID).withInitialPositionInStream(InitialPositionInStream.TRIM_HORIZON); 51 | 52 | startKCLWorker(workerConfig); 53 | 54 | while (recordProcessorFactory.getNumRecordsProcessed() < (2 * numItemsInSrcTable) /* Num of expected stream records */) { 55 | LOG.info("Sleep till all records are processed"); 56 | Thread.sleep(THREAD_SLEEP_2S); 57 | } 58 | 59 | shutDownKCLWorker(); 60 | 61 | ScanResult srcTableScan = TestUtil.scanTable(dynamoDBClient, srcTable); 62 | ScanResult destTableScan = TestUtil.scanTable(dynamoDBClient, destTable); 63 | assertEquals(srcTableScan.getItems(), destTableScan.getItems()); 64 | } 65 | 66 | @Test 67 | public void latestTest() throws Exception { 68 | LOG.info("Starting single shard KCL integration test with LATEST."); 69 | 70 | KinesisClientLibConfiguration workerConfig = 71 | new KinesisClientLibConfiguration(leaseTable, streamId, credentials, KCL_WORKER_ID).withInitialPositionInStream(InitialPositionInStream.LATEST); 72 | 73 | startKCLWorker(workerConfig); 74 | 75 | while (recordProcessorFactory.getNumRecordsProcessed() < 0) { 76 | LOG.info("Sleep till RecordProcessor is initialized"); 77 | Thread.sleep(THREAD_SLEEP_2S); 78 | } 79 | 80 | /* Only the following records will be processed by KCL since it is reading only the latest stream entries */ 81 | int numNewItemsToInsert = 1; 82 | insertAndUpdateItems(numNewItemsToInsert); 83 | 84 | while (recordProcessorFactory.getNumRecordsProcessed() < 2 * numNewItemsToInsert) { 85 | LOG.info("Sleep till all records are processed"); 86 | Thread.sleep(THREAD_SLEEP_2S); 87 | } 88 | 89 | shutDownKCLWorker(); 90 | 91 | String lastInsertedPartitionKey = Integer.toString(100 + this.numItemsInSrcTable); 92 | QueryResult srcTableQuery = TestUtil.queryTable(dynamoDBClient, srcTable, lastInsertedPartitionKey); 93 | ScanResult destTableScan = TestUtil.scanTable(dynamoDBClient, destTable); 94 | assertEquals(srcTableQuery.getItems(), destTableScan.getItems()); 95 | } 96 | 97 | /** 98 | * This test spawns a thread to periodically write items to the source table. It shuts down and restarts the KCL 99 | * worker while writes are happening (to simulate the real-world situation of a worker dying and another taking its 100 | * place). There are two things being verified here: 101 | * 1. New KCL worker resumes from the checkpoint 102 | * 2. All stream records are processed 103 | * 104 | * @throws Exception 105 | */ 106 | @Test 107 | public void workerFailureTest() throws Exception { 108 | LOG.info("Starting single shard KCL worker failure test."); 109 | 110 | KinesisClientLibConfiguration workerConfig = 111 | new KinesisClientLibConfiguration(leaseTable, streamId, credentials, KCL_WORKER_ID).withInitialPositionInStream(InitialPositionInStream.TRIM_HORIZON); 112 | 113 | startKCLWorker(workerConfig); 114 | 115 | // A thread that keeps writing to the table every 2 seconds 116 | ScheduledExecutorService loadGeneratorService = Executors.newSingleThreadScheduledExecutor(); 117 | loadGeneratorService.scheduleAtFixedRate(new Runnable() { 118 | 119 | @Override 120 | public void run() { 121 | insertAndUpdateItems(1); 122 | } 123 | }, 0/* initialDelay */, 2/* period */, TimeUnit.SECONDS); 124 | 125 | while (recordProcessorFactory.getNumRecordsProcessed() < 10) { 126 | LOG.info("Sleep till first few records are processed"); 127 | Thread.sleep(THREAD_SLEEP_2S); 128 | } 129 | 130 | shutDownKCLWorker(); 131 | 132 | // Calculate number of records processed by first worker and also the number of processed-but-not-checkpointed 133 | // records, since checkpoint happens after every batch of 10 records 134 | int numRecordsProcessedByFirstWorker = recordProcessorFactory.getNumRecordsProcessed(); 135 | int numRecordsNotCheckpointed = numRecordsProcessedByFirstWorker % ReplicatingRecordProcessor.CHECKPOINT_BATCH_SIZE; 136 | 137 | // Start a new worker 138 | startKCLWorker(workerConfig); 139 | 140 | while (recordProcessorFactory.getNumRecordsProcessed() < 0) { 141 | LOG.info("Sleep till RecordProcessor is initialized"); 142 | Thread.sleep(THREAD_SLEEP_2S); 143 | } 144 | 145 | loadGeneratorService.shutdown(); 146 | 147 | if (!loadGeneratorService.awaitTermination(THREAD_SLEEP_5S, TimeUnit.MILLISECONDS)) { 148 | loadGeneratorService.shutdownNow(); 149 | } 150 | 151 | int numStreamRecords = 2 * this.numItemsInSrcTable; 152 | int remainingRecordsToBeProcessed = numStreamRecords - numRecordsProcessedByFirstWorker + numRecordsNotCheckpointed; 153 | 154 | /* 155 | * The second worker must process atleast remainingRecordsToBeProcessed 156 | * num of records so that we have replicated everything to destination 157 | * table. Thus, this should never technically end up as an infinite 158 | * loop. If it does, something else is gone wrong. 159 | */ 160 | while (recordProcessorFactory.getNumRecordsProcessed() < remainingRecordsToBeProcessed) { 161 | LOG.info("Sleep till remaining records are processed"); 162 | Thread.sleep(THREAD_SLEEP_2S); 163 | } 164 | 165 | shutDownKCLWorker(); 166 | 167 | ScanResult srcTableScan = TestUtil.scanTable(dynamoDBClient, srcTable); 168 | ScanResult destTableScan = TestUtil.scanTable(dynamoDBClient, destTable); 169 | assertEquals(srcTableScan.getItems(), destTableScan.getItems()); 170 | } 171 | 172 | /** 173 | * This method will insert items sequentially with hash keys 101, 102 and so on. The updateItem call adds a new 174 | * attribute to the previously inserted item 175 | * 176 | * @param numItemsToInsert 177 | */ 178 | private void insertAndUpdateItems(int numItemsToInsert) { 179 | for (int i = 1; i <= numItemsToInsert; i++) { 180 | numItemsInSrcTable++; 181 | String partitionKey = Integer.toString(100 + numItemsInSrcTable); 182 | String attribute1 = partitionKey + "-attr1"; 183 | String attribute2 = partitionKey + "-attr2"; 184 | 185 | TestUtil.putItem(dynamoDBClient, srcTable, partitionKey, attribute1); 186 | TestUtil.updateItem(dynamoDBClient, srcTable, partitionKey, attribute2); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/functionals/FunctionalTestBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.functionals; 7 | 8 | import java.util.concurrent.ExecutorService; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import org.apache.commons.logging.Log; 13 | import org.apache.commons.logging.LogFactory; 14 | import org.junit.After; 15 | import org.junit.Before; 16 | 17 | import com.amazonaws.auth.AWSCredentialsProvider; 18 | import com.amazonaws.auth.BasicAWSCredentials; 19 | import com.amazonaws.internal.StaticCredentialsProvider; 20 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; 21 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDBStreams; 22 | import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded; 23 | import com.amazonaws.services.dynamodbv2.local.shared.access.AmazonDynamoDBLocal; 24 | import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; 25 | import com.amazonaws.services.dynamodbv2.streamsadapter.AmazonDynamoDBStreamsAdapterClient; 26 | import com.amazonaws.services.dynamodbv2.streamsadapter.StreamsWorkerFactory; 27 | import com.amazonaws.services.dynamodbv2.streamsadapter.util.TestRecordProcessorFactory; 28 | import com.amazonaws.services.dynamodbv2.streamsadapter.util.TestUtil; 29 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.KinesisClientLibConfiguration; 30 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.Worker; 31 | import com.amazonaws.services.kinesis.metrics.impl.NullMetricsFactory; 32 | 33 | /** 34 | * This base class sets up DynamoDB, Kinesis Adapter and DynamoDB streams clients used by a KCL worker operating on DynamoDB 35 | * Streams. It also creates required DynamoDB tables. 36 | */ 37 | public abstract class FunctionalTestBase { 38 | private static final Log LOG = LogFactory.getLog(FunctionalTestBase.class); 39 | 40 | protected AmazonDynamoDBLocal dynamoDBLocal; 41 | protected AmazonDynamoDBStreams streamsClient; 42 | protected AmazonDynamoDBStreamsAdapterClient adapterClient; 43 | protected AmazonDynamoDB dynamoDBClient; 44 | 45 | protected AWSCredentialsProvider credentials; 46 | protected String streamId; 47 | 48 | protected Worker worker; 49 | protected TestRecordProcessorFactory recordProcessorFactory; 50 | protected ExecutorService workerThread; 51 | 52 | private static String accessKeyId = "KCLIntegTest"; 53 | private static String secretAccessKey = "dummy"; 54 | 55 | protected static String serviceName = "dynamodb"; 56 | protected static String dynamodbEndpoint = "dummyEndpoint"; 57 | 58 | protected static String srcTable = "kcl-integ-test-src"; 59 | protected static String destTable = "kcl-integ-test-dest"; 60 | protected static String leaseTable = "kcl-integ-test-leases"; 61 | 62 | protected static int THREAD_SLEEP_5S = 5000; 63 | protected static int THREAD_SLEEP_2S = 2000; 64 | protected static String KCL_WORKER_ID = "kcl-integration-test-worker"; 65 | 66 | @Before 67 | public void setup() { 68 | credentials = new StaticCredentialsProvider(new BasicAWSCredentials(accessKeyId, secretAccessKey)); 69 | 70 | dynamoDBLocal = DynamoDBEmbedded.create(); 71 | dynamoDBClient = dynamoDBLocal.amazonDynamoDB(); 72 | streamsClient = dynamoDBLocal.amazonDynamoDBStreams(); 73 | 74 | adapterClient = new AmazonDynamoDBStreamsAdapterClient(streamsClient); 75 | 76 | streamId = TestUtil.createTable(dynamoDBClient, srcTable, true /*With streams enabled*/); 77 | TestUtil.createTable(dynamoDBClient, destTable, false /* No streams */); 78 | 79 | TestUtil.waitForTableActive(dynamoDBClient, srcTable); 80 | TestUtil.waitForTableActive(dynamoDBClient, destTable); 81 | } 82 | 83 | @After 84 | public void teardown() { 85 | dynamoDBClient.deleteTable(new DeleteTableRequest().withTableName(srcTable)); 86 | dynamoDBClient.deleteTable(new DeleteTableRequest().withTableName(destTable)); 87 | dynamoDBClient.deleteTable(new DeleteTableRequest().withTableName(leaseTable)); 88 | 89 | dynamoDBLocal.shutdown(); 90 | } 91 | 92 | protected void startKCLWorker(KinesisClientLibConfiguration workerConfig) { 93 | 94 | recordProcessorFactory = new TestRecordProcessorFactory(dynamoDBClient, destTable); 95 | 96 | LOG.info("Creating worker for stream: " + streamId); 97 | worker = StreamsWorkerFactory 98 | .createDynamoDbStreamsWorker(recordProcessorFactory, workerConfig, adapterClient, dynamoDBClient, new NullMetricsFactory(), Executors.newCachedThreadPool()); 99 | 100 | LOG.info("Starting worker..."); 101 | workerThread = Executors.newSingleThreadExecutor(); 102 | workerThread.submit(worker); 103 | 104 | workerThread.shutdown(); //This will wait till the KCL worker exits 105 | } 106 | 107 | protected void shutDownKCLWorker() throws Exception { 108 | worker.shutdown(); 109 | 110 | if (!workerThread.awaitTermination(THREAD_SLEEP_5S, TimeUnit.MILLISECONDS)) { 111 | workerThread.shutdownNow(); 112 | } 113 | 114 | LOG.info("Processing complete."); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/functionals/KinesisParametersTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.functionals; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertTrue; 10 | 11 | import com.amazonaws.services.dynamodbv2.model.BillingMode; 12 | import com.amazonaws.services.dynamodbv2.model.BillingModeSummary; 13 | import org.apache.commons.logging.Log; 14 | import org.apache.commons.logging.LogFactory; 15 | import org.junit.Test; 16 | 17 | import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; 18 | import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughputDescription; 19 | import com.amazonaws.services.dynamodbv2.model.TableDescription; 20 | import com.amazonaws.services.dynamodbv2.streamsadapter.util.TestRecordProcessorFactory; 21 | import com.amazonaws.services.dynamodbv2.streamsadapter.util.TestUtil; 22 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.InitialPositionInStream; 23 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.KinesisClientLibConfiguration; 24 | 25 | public class KinesisParametersTest extends FunctionalTestBase { 26 | private static final Log LOG = LogFactory.getLog(KinesisParametersTest.class); 27 | 28 | private static String KCL_WORKER_ID = "kcl-integration-test-worker"; 29 | private static long IDLE_TIME_2S = 2000L; 30 | 31 | @Test 32 | public void leaseTableThroughputTest() throws Exception { 33 | KinesisClientLibConfiguration workerConfig = 34 | new KinesisClientLibConfiguration(leaseTable, streamId, credentials, KCL_WORKER_ID).withInitialPositionInStream(InitialPositionInStream.TRIM_HORIZON) 35 | .withInitialLeaseTableReadCapacity(50).withInitialLeaseTableWriteCapacity(50); 36 | 37 | startKCLWorker(workerConfig); 38 | 39 | while (((TestRecordProcessorFactory) recordProcessorFactory).getNumRecordsProcessed() < 0) { 40 | LOG.info("Sleep till RecordProcessor is initialized"); 41 | Thread.sleep(THREAD_SLEEP_2S); 42 | } 43 | 44 | shutDownKCLWorker(); 45 | 46 | DescribeTableResult describeTableResult = TestUtil.describeTable(dynamoDBClient, leaseTable); 47 | TableDescription leaseTableDescription = describeTableResult.getTable(); 48 | ProvisionedThroughputDescription leaseTableThroughput = leaseTableDescription.getProvisionedThroughput(); 49 | 50 | assertEquals(new Long(50), leaseTableThroughput.getReadCapacityUnits()); 51 | assertEquals(new Long(50), leaseTableThroughput.getWriteCapacityUnits()); 52 | } 53 | 54 | /** 55 | * This test configures KCL to call processRecords even when getRecords call returns nothing. The idle time setting 56 | * determines how many getRecords() calls will be made per second 57 | * 58 | * @throws Exception 59 | */ 60 | @Test 61 | public void numProcessRecordsCallsTest() throws Exception { 62 | KinesisClientLibConfiguration workerConfig = 63 | new KinesisClientLibConfiguration(leaseTable, streamId, credentials, KCL_WORKER_ID).withMaxRecords(10).withInitialPositionInStream(InitialPositionInStream.TRIM_HORIZON) 64 | .withCallProcessRecordsEvenForEmptyRecordList(true).withIdleTimeBetweenReadsInMillis(IDLE_TIME_2S); 65 | 66 | startKCLWorker(workerConfig); 67 | 68 | while (((TestRecordProcessorFactory) recordProcessorFactory).getNumRecordsProcessed() < 0) { 69 | LOG.info("Sleep till RecordProcessor is initialized"); 70 | Thread.sleep(THREAD_SLEEP_2S); 71 | } 72 | 73 | // Let KCL run for another 5 seconds 74 | Thread.sleep(THREAD_SLEEP_5S); 75 | 76 | shutDownKCLWorker(); 77 | 78 | int numGetRecordsCalls = recordProcessorFactory.getNumProcessRecordsCalls(); 79 | 80 | LOG.info("Num getRecords calls: " + numGetRecordsCalls); 81 | // Atleast 1 and atmost 2 getRecords/processRecords calls should have been made 82 | assertTrue(numGetRecordsCalls > 0 && numGetRecordsCalls <= 3); 83 | } 84 | 85 | /** 86 | * This test configures the worker with a non-default billing mode and ensures that the billing mode is passed 87 | * through to the created lease table. 88 | */ 89 | @Test 90 | public void billingModeTest() throws Exception { 91 | KinesisClientLibConfiguration workerConfig = 92 | new KinesisClientLibConfiguration(leaseTable, streamId, credentials, KCL_WORKER_ID) 93 | .withBillingMode(BillingMode.PAY_PER_REQUEST); 94 | 95 | startKCLWorker(workerConfig); 96 | 97 | while (recordProcessorFactory.getNumRecordsProcessed() < 0) { 98 | LOG.info("Sleep till RecordProcessor is initialized"); 99 | Thread.sleep(THREAD_SLEEP_2S); 100 | } 101 | 102 | shutDownKCLWorker(); 103 | 104 | DescribeTableResult describeTableResult = TestUtil.describeTable(dynamoDBClient, leaseTable); 105 | TableDescription leaseTableDescription = describeTableResult.getTable(); 106 | BillingModeSummary billingModeSummary = leaseTableDescription.getBillingModeSummary(); 107 | assertEquals(BillingMode.PAY_PER_REQUEST.toString(), billingModeSummary.getBillingMode()); 108 | } 109 | } -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/DescribeStreamRequestAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.mockito.Mockito.times; 10 | import static org.mockito.Mockito.verify; 11 | import static org.mockito.Mockito.when; 12 | 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.mockito.Mock; 16 | import org.mockito.MockitoAnnotations; 17 | 18 | import com.amazonaws.services.kinesis.model.DescribeStreamRequest; 19 | 20 | public class DescribeStreamRequestAdapterTest { 21 | private final String TEST_STRING = "TestString"; 22 | private final Integer TEST_INT = 42; 23 | 24 | @Mock 25 | private DescribeStreamRequest mockRequest; 26 | 27 | private DescribeStreamRequestAdapter adapter; 28 | 29 | @Before 30 | public void setUpTest() { 31 | MockitoAnnotations.initMocks(this); 32 | adapter = new DescribeStreamRequestAdapter(mockRequest); 33 | } 34 | 35 | @Test 36 | public void testGetExclusiveStartShardId() { 37 | when(mockRequest.getExclusiveStartShardId()).thenReturn(TEST_STRING); 38 | String actual = adapter.getExclusiveStartShardId(); 39 | assertEquals(TEST_STRING, actual); 40 | } 41 | 42 | @Test 43 | public void testSetExclusiveStartShardId() { 44 | adapter.setExclusiveStartShardId(TEST_STRING); 45 | verify(mockRequest, times(1)).setExclusiveStartShardId(TEST_STRING); 46 | } 47 | 48 | @Test 49 | public void testWithExclusiveStartShardId() { 50 | Object actual = adapter.withExclusiveStartShardId(TEST_STRING); 51 | assertEquals(adapter, actual); 52 | } 53 | 54 | @Test 55 | public void testGetLimit() { 56 | when(mockRequest.getLimit()).thenReturn(TEST_INT); 57 | Integer actual = adapter.getLimit(); 58 | assertEquals(TEST_INT, actual); 59 | } 60 | 61 | @Test 62 | public void testSetLimit() { 63 | adapter.setLimit(TEST_INT); 64 | verify(mockRequest, times(1)).setLimit(TEST_INT); 65 | } 66 | 67 | @Test 68 | public void testWithLimit() { 69 | Object actual = adapter.withLimit(TEST_INT); 70 | assertEquals(adapter, actual); 71 | } 72 | 73 | @Test 74 | public void testGetStreamArn() { 75 | when(mockRequest.getStreamName()).thenReturn(TEST_STRING); 76 | String actual = adapter.getStreamArn(); 77 | assertEquals(TEST_STRING, actual); 78 | } 79 | 80 | @Test 81 | public void testSetStreamArn() { 82 | adapter.setStreamArn(TEST_STRING); 83 | verify(mockRequest, times(1)).setStreamName(TEST_STRING); 84 | } 85 | 86 | @Test 87 | public void testWithStreamArn() { 88 | Object actual = adapter.withStreamArn(TEST_STRING); 89 | assertEquals(adapter, actual); 90 | } 91 | 92 | @Test 93 | public void testRealData() { 94 | DescribeStreamRequest request = createRequest(); 95 | DescribeStreamRequestAdapter requestAdapter = new DescribeStreamRequestAdapter(request); 96 | assertEquals(request.getExclusiveStartShardId(), requestAdapter.getExclusiveStartShardId()); 97 | assertEquals(request.getLimit(), requestAdapter.getLimit()); 98 | assertEquals(request.getStreamName(), requestAdapter.getStreamArn()); 99 | } 100 | 101 | private DescribeStreamRequest createRequest() { 102 | return new DescribeStreamRequest().withExclusiveStartShardId(TEST_STRING).withLimit(TEST_INT).withStreamName(TEST_STRING); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/DescribeStreamResultAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertTrue; 9 | import static org.mockito.Mockito.when; 10 | 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.mockito.Mock; 14 | import org.mockito.MockitoAnnotations; 15 | 16 | import com.amazonaws.services.dynamodbv2.model.DescribeStreamResult; 17 | import com.amazonaws.services.dynamodbv2.model.StreamDescription; 18 | 19 | public class DescribeStreamResultAdapterTest { 20 | 21 | @Mock 22 | private DescribeStreamResult mockResult; 23 | 24 | @Mock 25 | private StreamDescription mockDescription; 26 | 27 | private DescribeStreamResultAdapter adapter; 28 | 29 | @Before 30 | public void setUpTest() { 31 | MockitoAnnotations.initMocks(this); 32 | when(mockResult.getStreamDescription()).thenReturn(mockDescription); 33 | adapter = new DescribeStreamResultAdapter(mockResult); 34 | } 35 | 36 | @Test 37 | public void testGetStreamDescription() { 38 | Object streamDescription = adapter.getStreamDescription(); 39 | assertTrue(streamDescription instanceof StreamDescriptionAdapter); 40 | } 41 | 42 | @Test(expected = UnsupportedOperationException.class) 43 | public void testSetStreamDescription() { 44 | adapter.setStreamDescription(null); 45 | } 46 | 47 | @Test(expected = UnsupportedOperationException.class) 48 | public void testWithStreamDescription() { 49 | adapter.withStreamDescription(null); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/GetRecordsRequestAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.mockito.Mockito.times; 10 | import static org.mockito.Mockito.verify; 11 | import static org.mockito.Mockito.when; 12 | 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.mockito.Mock; 16 | import org.mockito.MockitoAnnotations; 17 | 18 | import com.amazonaws.services.kinesis.model.GetRecordsRequest; 19 | 20 | public class GetRecordsRequestAdapterTest { 21 | private final String TEST_STRING = "TestString"; 22 | private final Integer TEST_INT = 42; 23 | 24 | @Mock 25 | private GetRecordsRequest mockRequest; 26 | 27 | private GetRecordsRequestAdapter adapter; 28 | 29 | @Before 30 | public void setUpTest() { 31 | MockitoAnnotations.initMocks(this); 32 | adapter = new GetRecordsRequestAdapter(mockRequest); 33 | } 34 | 35 | @Test 36 | public void testGetLimit() { 37 | when(mockRequest.getLimit()).thenReturn(TEST_INT); 38 | Integer actual = adapter.getLimit(); 39 | assertEquals(TEST_INT, actual); 40 | } 41 | 42 | @Test 43 | public void testSetLimit() { 44 | adapter.setLimit(TEST_INT); 45 | verify(mockRequest, times(1)).setLimit(TEST_INT); 46 | } 47 | 48 | @Test 49 | public void testWithLimit() { 50 | Object actual = adapter.withLimit(TEST_INT); 51 | assertEquals(adapter, actual); 52 | } 53 | 54 | @Test 55 | public void testGetShardIterator() { 56 | when(mockRequest.getShardIterator()).thenReturn(TEST_STRING); 57 | String actual = adapter.getShardIterator(); 58 | assertEquals(TEST_STRING, actual); 59 | } 60 | 61 | @Test 62 | public void testSetShardIterator() { 63 | adapter.setShardIterator(TEST_STRING); 64 | verify(mockRequest, times(1)).setShardIterator(TEST_STRING); 65 | } 66 | 67 | @Test 68 | public void testWithShardIterator() { 69 | Object actual = adapter.withShardIterator(TEST_STRING); 70 | assertEquals(adapter, actual); 71 | } 72 | 73 | @Test 74 | public void testRealData() { 75 | GetRecordsRequest request = createRequest(); 76 | GetRecordsRequestAdapter requestAdapter = new GetRecordsRequestAdapter(request); 77 | assertEquals(request.getShardIterator(), requestAdapter.getShardIterator()); 78 | assertEquals(request.getLimit(), requestAdapter.getLimit()); 79 | } 80 | 81 | private GetRecordsRequest createRequest() { 82 | return new GetRecordsRequest().withLimit(TEST_INT).withShardIterator(TEST_STRING); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/GetRecordsResultAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertTrue; 10 | import static org.mockito.Mockito.when; 11 | 12 | import java.util.Collection; 13 | import java.util.Collections; 14 | 15 | import org.junit.Before; 16 | import org.junit.Test; 17 | import org.mockito.Mock; 18 | import org.mockito.MockitoAnnotations; 19 | 20 | import com.amazonaws.services.dynamodbv2.model.GetRecordsResult; 21 | import com.amazonaws.services.dynamodbv2.model.Record; 22 | 23 | public class GetRecordsResultAdapterTest { 24 | private final String TEST_STRING = "TestString"; 25 | 26 | @Mock 27 | private GetRecordsResult mockResult; 28 | 29 | private GetRecordsResultAdapter adapter; 30 | 31 | @Before 32 | public void setUpTest() { 33 | MockitoAnnotations.initMocks(this); 34 | java.util.List records = new java.util.ArrayList(); 35 | records.add(new Record()); 36 | when(mockResult.getRecords()).thenReturn(records); 37 | adapter = new GetRecordsResultAdapter(mockResult); 38 | } 39 | 40 | @Test 41 | public void testGetNextShardIterator() { 42 | when(mockResult.getNextShardIterator()).thenReturn(TEST_STRING); 43 | String actual = adapter.getNextShardIterator(); 44 | assertEquals(TEST_STRING, actual); 45 | } 46 | 47 | @Test(expected = UnsupportedOperationException.class) 48 | public void testSetNextShardIterator() { 49 | adapter.setNextShardIterator(TEST_STRING); 50 | } 51 | 52 | @Test(expected = UnsupportedOperationException.class) 53 | public void testWithNextShardIterator() { 54 | adapter.withNextShardIterator(TEST_STRING); 55 | } 56 | 57 | @Test 58 | public void testGetRecordsWithItem() { 59 | java.util.List recordList = adapter.getRecords(); 60 | assertEquals(1, recordList.size()); 61 | assertTrue(recordList.get(0) instanceof RecordAdapter); 62 | } 63 | 64 | @Test 65 | public void testGetRecordsWithNoItems() { 66 | when(mockResult.getRecords()).thenReturn(new java.util.ArrayList()); 67 | GetRecordsResultAdapter localAdapter = new GetRecordsResultAdapter(mockResult); 68 | java.util.List recordList = localAdapter.getRecords(); 69 | assertEquals(0, recordList.size()); 70 | } 71 | 72 | @Test 73 | public void testGetRecordsWithNull() { 74 | when(mockResult.getRecords()).thenReturn(null); 75 | GetRecordsResultAdapter localAdapter = new GetRecordsResultAdapter(mockResult); 76 | java.util.List recordList = localAdapter.getRecords(); 77 | assertEquals(0, recordList.size()); 78 | } 79 | 80 | @Test(expected = UnsupportedOperationException.class) 81 | public void testSetRecords() { 82 | adapter.setRecords(null); 83 | } 84 | 85 | @Test(expected = UnsupportedOperationException.class) 86 | public void testWithRecords() { 87 | adapter.withRecords(null, null); 88 | } 89 | 90 | @Test(expected = UnsupportedOperationException.class) 91 | public void testWithRecords2() { 92 | Collection records = Collections.emptyList(); 93 | adapter.withRecords(records); 94 | } 95 | 96 | @Test 97 | public void testRealDataNoRecords() { 98 | GetRecordsResult result = createResult(false); 99 | GetRecordsResultAdapter resultAdapter = new GetRecordsResultAdapter(result); 100 | assertEquals(result.getNextShardIterator(), resultAdapter.getNextShardIterator()); 101 | assertEquals(result.getRecords().size(), resultAdapter.getRecords().size()); 102 | } 103 | 104 | @Test 105 | public void testRealDataWithRecords() { 106 | GetRecordsResult result = createResult(true); 107 | GetRecordsResultAdapter resultAdapter = new GetRecordsResultAdapter(result); 108 | assertEquals(result.getNextShardIterator(), resultAdapter.getNextShardIterator()); 109 | assertEquals(result.getRecords().size(), resultAdapter.getRecords().size()); 110 | } 111 | 112 | private GetRecordsResult createResult(Boolean withRecord) { 113 | java.util.List records = new java.util.ArrayList(); 114 | if (withRecord) { 115 | records.add(new Record()); 116 | } 117 | return new GetRecordsResult().withRecords(records).withNextShardIterator(TEST_STRING); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/GetShardIteratorRequestAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.mockito.Mockito.times; 10 | import static org.mockito.Mockito.verify; 11 | import static org.mockito.Mockito.when; 12 | 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.mockito.Mock; 16 | import org.mockito.MockitoAnnotations; 17 | 18 | import com.amazonaws.services.kinesis.model.GetShardIteratorRequest; 19 | 20 | public class GetShardIteratorRequestAdapterTest { 21 | private final String TEST_STRING = "TestString"; 22 | 23 | @Mock 24 | private GetShardIteratorRequest mockRequest; 25 | 26 | private GetShardIteratorRequestAdapter adapter; 27 | 28 | @Before 29 | public void setUpTest() { 30 | MockitoAnnotations.initMocks(this); 31 | adapter = new GetShardIteratorRequestAdapter(mockRequest); 32 | } 33 | 34 | @Test 35 | public void testGetStreamArn() { 36 | when(mockRequest.getStreamName()).thenReturn(TEST_STRING); 37 | String actual = adapter.getStreamArn(); 38 | assertEquals(TEST_STRING, actual); 39 | } 40 | 41 | @Test 42 | public void testSetStreamArn() { 43 | adapter.setStreamArn(TEST_STRING); 44 | verify(mockRequest, times(1)).setStreamName(TEST_STRING); 45 | } 46 | 47 | @Test 48 | public void testWithStreamArn() { 49 | Object actual = adapter.withStreamArn(TEST_STRING); 50 | assertEquals(adapter, actual); 51 | } 52 | 53 | @Test 54 | public void testGetShardId() { 55 | when(mockRequest.getShardId()).thenReturn(TEST_STRING); 56 | String actual = adapter.getShardId(); 57 | assertEquals(TEST_STRING, actual); 58 | } 59 | 60 | @Test 61 | public void testSetShardId() { 62 | adapter.setShardId(TEST_STRING); 63 | verify(mockRequest, times(1)).setShardId(TEST_STRING); 64 | } 65 | 66 | @Test 67 | public void testWithShardId() { 68 | Object actual = adapter.withShardId(TEST_STRING); 69 | assertEquals(adapter, actual); 70 | } 71 | 72 | @Test 73 | public void testGetSequenceNumber() { 74 | when(mockRequest.getStartingSequenceNumber()).thenReturn(TEST_STRING); 75 | String actual = adapter.getSequenceNumber(); 76 | assertEquals(TEST_STRING, actual); 77 | } 78 | 79 | @Test 80 | public void testSetSequenceNumber() { 81 | adapter.setSequenceNumber(TEST_STRING); 82 | verify(mockRequest, times(1)).setStartingSequenceNumber(TEST_STRING); 83 | } 84 | 85 | @Test 86 | public void testWithSequenceNumber() { 87 | Object actual = adapter.withSequenceNumber(TEST_STRING); 88 | assertEquals(adapter, actual); 89 | } 90 | 91 | @Test 92 | public void testGetShardIteratorType() { 93 | when(mockRequest.getShardIteratorType()).thenReturn(TEST_STRING); 94 | String actual = adapter.getShardIteratorType(); 95 | assertEquals(TEST_STRING, actual); 96 | } 97 | 98 | @Test(expected = IllegalArgumentException.class) 99 | public void testSetShardIteratorTypeFailure() { 100 | adapter.setShardIteratorType(TEST_STRING); 101 | } 102 | 103 | @Test 104 | public void testSetShardIteratorTypeAsTypeAfterSequenceNumber() { 105 | adapter.setShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.AFTER_SEQUENCE_NUMBER); 106 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.AFTER_SEQUENCE_NUMBER.toString()); 107 | } 108 | 109 | @Test 110 | public void testSetShardIteratorTypeAsTypeAtSequenceNumber() { 111 | adapter.setShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.AT_SEQUENCE_NUMBER); 112 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.AT_SEQUENCE_NUMBER.toString()); 113 | } 114 | 115 | @Test 116 | public void testSetShardIteratorTypeAsTypeLatest() { 117 | adapter.setShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.LATEST); 118 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.LATEST.toString()); 119 | } 120 | 121 | @Test 122 | public void testSetShardIteratorTypeAsTypeTrimHorizon() { 123 | adapter.setShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.TRIM_HORIZON); 124 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.TRIM_HORIZON.toString()); 125 | } 126 | 127 | @Test(expected = IllegalArgumentException.class) 128 | public void testWithShardIteratorTypeFailure() { 129 | adapter.withShardIteratorType(TEST_STRING); 130 | } 131 | 132 | @Test 133 | public void testWithShardIteratorTypeAsStringAfterSequenceNumber() { 134 | adapter.withShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.AFTER_SEQUENCE_NUMBER.toString()); 135 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.AFTER_SEQUENCE_NUMBER.toString()); 136 | } 137 | 138 | @Test 139 | public void testWithShardIteratorTypeAsStringAtSequenceNumber() { 140 | adapter.withShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.AT_SEQUENCE_NUMBER.toString()); 141 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.AT_SEQUENCE_NUMBER.toString()); 142 | } 143 | 144 | @Test 145 | public void testWithShardIteratorTypeAsStringLatest() { 146 | adapter.withShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.LATEST.toString()); 147 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.LATEST.toString()); 148 | } 149 | 150 | @Test 151 | public void testWithShardIteratorTypeAsStringTrimHorizon() { 152 | adapter.withShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.TRIM_HORIZON.toString()); 153 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.TRIM_HORIZON.toString()); 154 | } 155 | 156 | @Test 157 | public void testSetShardIteratorTypeAsStringAfterSequenceNumber() { 158 | adapter.setShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.AFTER_SEQUENCE_NUMBER.toString()); 159 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.AFTER_SEQUENCE_NUMBER.toString()); 160 | } 161 | 162 | @Test 163 | public void testSetShardIteratorTypeAsStringAtSequenceNumber() { 164 | adapter.setShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.AT_SEQUENCE_NUMBER.toString()); 165 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.AT_SEQUENCE_NUMBER.toString()); 166 | } 167 | 168 | @Test 169 | public void testSetShardIteratorTypeAsStringLatest() { 170 | adapter.setShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.LATEST.toString()); 171 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.LATEST.toString()); 172 | } 173 | 174 | @Test 175 | public void testSetShardIteratorTypeAsStringTrimHorizon() { 176 | adapter.setShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.TRIM_HORIZON.toString()); 177 | verify(mockRequest, times(1)).setShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.TRIM_HORIZON.toString()); 178 | } 179 | 180 | @Test 181 | public void testWithShardIteratorTypeAsType() { 182 | Object actual = adapter.withShardIteratorType(com.amazonaws.services.dynamodbv2.model.ShardIteratorType.LATEST); 183 | assertEquals(adapter, actual); 184 | } 185 | 186 | @Test 187 | public void testRealData() { 188 | GetShardIteratorRequest request = createRequest(); 189 | GetShardIteratorRequestAdapter requestAdapter = new GetShardIteratorRequestAdapter(request); 190 | assertEquals(request.getStartingSequenceNumber(), requestAdapter.getSequenceNumber()); 191 | assertEquals(request.getShardId(), requestAdapter.getShardId()); 192 | assertEquals(request.getShardIteratorType(), requestAdapter.getShardIteratorType()); 193 | assertEquals(request.getStreamName(), requestAdapter.getStreamArn()); 194 | } 195 | 196 | private GetShardIteratorRequest createRequest() { 197 | return new GetShardIteratorRequest() 198 | .withShardId(TEST_STRING) 199 | .withStartingSequenceNumber(TEST_STRING) 200 | .withShardIteratorType(com.amazonaws.services.kinesis.model.ShardIteratorType.LATEST) 201 | .withStreamName(TEST_STRING); 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/GetShardIteratorResultAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.mockito.Mockito.when; 10 | 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.mockito.Mock; 14 | import org.mockito.MockitoAnnotations; 15 | 16 | import com.amazonaws.services.dynamodbv2.model.GetShardIteratorResult; 17 | 18 | public class GetShardIteratorResultAdapterTest { 19 | private final String TEST_STRING = "TestString"; 20 | 21 | @Mock 22 | private GetShardIteratorResult mockResult; 23 | 24 | private GetShardIteratorResultAdapter adapter; 25 | 26 | @Before 27 | public void setUpTest() { 28 | MockitoAnnotations.initMocks(this); 29 | adapter = new GetShardIteratorResultAdapter(mockResult); 30 | } 31 | 32 | @Test 33 | public void testGetShardIterator() { 34 | when(mockResult.getShardIterator()).thenReturn(TEST_STRING); 35 | String actual = adapter.getShardIterator(); 36 | assertEquals(TEST_STRING, actual); 37 | } 38 | 39 | @Test(expected = UnsupportedOperationException.class) 40 | public void testSetShardIterator() { 41 | adapter.setShardIterator(TEST_STRING); 42 | } 43 | 44 | @Test(expected = UnsupportedOperationException.class) 45 | public void testWithShardIterator() { 46 | adapter.withShardIterator(TEST_STRING); 47 | } 48 | 49 | @Test 50 | public void testRealData() { 51 | GetShardIteratorResult result = createResult(); 52 | GetShardIteratorResultAdapter resultAdapter = new GetShardIteratorResultAdapter(result); 53 | assertEquals(result.getShardIterator(), resultAdapter.getShardIterator()); 54 | } 55 | 56 | private GetShardIteratorResult createResult() { 57 | return new GetShardIteratorResult().withShardIterator(TEST_STRING); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/ListStreamsRequestAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.mockito.Mockito.verify; 10 | import static org.mockito.Mockito.when; 11 | 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.mockito.Mock; 15 | import org.mockito.MockitoAnnotations; 16 | 17 | import com.amazonaws.services.kinesis.model.ListStreamsRequest; 18 | 19 | public class ListStreamsRequestAdapterTest { 20 | private final String TEST_STRING = "TestString"; 21 | 22 | private final Integer TEST_INT = 42; 23 | 24 | @Mock 25 | private ListStreamsRequest mockRequest; 26 | 27 | private ListStreamsRequestAdapter adapter; 28 | 29 | @Before 30 | public void setUpTest() { 31 | MockitoAnnotations.initMocks(this); 32 | adapter = new ListStreamsRequestAdapter(mockRequest); 33 | } 34 | 35 | @Test 36 | public void testGetExclusiveStartStreamArn() { 37 | when(mockRequest.getExclusiveStartStreamName()).thenReturn(TEST_STRING); 38 | String actual = adapter.getExclusiveStartStreamArn(); 39 | assertEquals(TEST_STRING, actual); 40 | } 41 | 42 | @Test 43 | public void testSetExclusiveStartStreamArn() { 44 | adapter.setExclusiveStartStreamArn(TEST_STRING); 45 | verify(mockRequest).setExclusiveStartStreamName(TEST_STRING); 46 | } 47 | 48 | @Test 49 | public void testWithExclusiveStartStreamArn() { 50 | Object actual = adapter.withExclusiveStartStreamArn(TEST_STRING); 51 | assertEquals(adapter, actual); 52 | } 53 | 54 | @Test(expected = UnsupportedOperationException.class) 55 | public void testGetTableName() { 56 | adapter.getTableName(); 57 | } 58 | 59 | @Test(expected = UnsupportedOperationException.class) 60 | public void testSetTableName() { 61 | adapter.setTableName(TEST_STRING); 62 | } 63 | 64 | @Test(expected = UnsupportedOperationException.class) 65 | public void testWithTableName() { 66 | adapter.withTableName(TEST_STRING); 67 | } 68 | 69 | @Test 70 | public void testGetLimit() { 71 | when(mockRequest.getLimit()).thenReturn(TEST_INT); 72 | Integer actual = adapter.getLimit(); 73 | assertEquals(TEST_INT, actual); 74 | } 75 | 76 | @Test 77 | public void testSetLimit() { 78 | adapter.setLimit(TEST_INT); 79 | verify(mockRequest).setLimit(TEST_INT); 80 | } 81 | 82 | @Test 83 | public void testWithLimit() { 84 | Object actual = adapter.withLimit(TEST_INT); 85 | assertEquals(adapter, actual); 86 | } 87 | 88 | @Test 89 | public void testRealData() { 90 | ListStreamsRequest request = createRequest(); 91 | ListStreamsRequestAdapter requestAdapter = new ListStreamsRequestAdapter(request); 92 | assertEquals(request.getExclusiveStartStreamName(), requestAdapter.getExclusiveStartStreamArn()); 93 | assertEquals(request.getLimit(), requestAdapter.getLimit()); 94 | } 95 | 96 | private ListStreamsRequest createRequest() { 97 | return new ListStreamsRequest().withExclusiveStartStreamName(TEST_STRING).withLimit(TEST_INT); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/ListStreamsResultAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertFalse; 10 | import static org.junit.Assert.assertTrue; 11 | import static org.mockito.Mockito.when; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.Collections; 16 | import java.util.List; 17 | 18 | import org.junit.Before; 19 | import org.junit.Test; 20 | import org.mockito.Mock; 21 | import org.mockito.MockitoAnnotations; 22 | 23 | import com.amazonaws.services.dynamodbv2.model.ListStreamsResult; 24 | import com.amazonaws.services.dynamodbv2.model.Stream; 25 | 26 | public class ListStreamsResultAdapterTest { 27 | private final String TEST_STRING = "TestString"; 28 | 29 | @Mock 30 | private ListStreamsResult mockResult; 31 | 32 | private ListStreamsResultAdapter adapter; 33 | 34 | @Before 35 | public void setUpTest() { 36 | MockitoAnnotations.initMocks(this); 37 | adapter = new ListStreamsResultAdapter(mockResult); 38 | } 39 | 40 | @Test 41 | public void testGetStreamNamesWithNoItems() { 42 | when(mockResult.getStreams()).thenReturn(new java.util.ArrayList()); 43 | java.util.List actual = adapter.getStreamNames(); 44 | assertTrue(actual.isEmpty()); 45 | } 46 | 47 | @Test 48 | public void testGetStreamNamesWithItem() { 49 | java.util.List streamList = new java.util.ArrayList<>(); 50 | Stream stream = new Stream(); 51 | stream.setStreamArn(TEST_STRING); 52 | streamList.add(stream); 53 | when(mockResult.getStreams()).thenReturn(streamList); 54 | 55 | java.util.List actual = adapter.getStreamNames(); 56 | assertTrue(actual.size() == 1); 57 | assertEquals(TEST_STRING, actual.get(0)); 58 | } 59 | 60 | @Test(expected = UnsupportedOperationException.class) 61 | public void testSetStreamNames() { 62 | adapter.setStreamNames(new java.util.ArrayList()); 63 | } 64 | 65 | @Test(expected = UnsupportedOperationException.class) 66 | public void testWithStreamNames() { 67 | adapter.withStreamNames(null, null); 68 | } 69 | 70 | @Test(expected = UnsupportedOperationException.class) 71 | public void testWithStreamNames2() { 72 | final Collection streamNames = Collections.emptyList(); 73 | adapter.withStreamNames(streamNames); 74 | } 75 | 76 | @Test 77 | public void testGetHasMoreStreamsTrue() { 78 | when(mockResult.getLastEvaluatedStreamArn()).thenReturn(TEST_STRING); 79 | assertTrue(adapter.getHasMoreStreams()); 80 | } 81 | 82 | @Test 83 | public void testGetHasMoreStreamsFalse() { 84 | when(mockResult.getLastEvaluatedStreamArn()).thenReturn(null); 85 | assertFalse(adapter.getHasMoreStreams()); 86 | } 87 | 88 | @Test 89 | public void testIsHasMoreStreamsTrue() { 90 | when(mockResult.getLastEvaluatedStreamArn()).thenReturn(TEST_STRING); 91 | assertTrue(adapter.isHasMoreStreams()); 92 | } 93 | 94 | @Test 95 | public void testIsHasMoreStreamsFalse() { 96 | when(mockResult.getLastEvaluatedStreamArn()).thenReturn(null); 97 | assertFalse(adapter.isHasMoreStreams()); 98 | } 99 | 100 | @Test(expected = UnsupportedOperationException.class) 101 | public void testSetHasMoreStreams() { 102 | adapter.setHasMoreStreams(false); 103 | } 104 | 105 | @Test(expected = UnsupportedOperationException.class) 106 | public void testWithHasMoreStreams() { 107 | adapter.withHasMoreStreams(false); 108 | } 109 | 110 | @Test 111 | public void testRealDataNoIds() { 112 | ListStreamsResult result = createResult(false); 113 | ListStreamsResultAdapter resultAdapter = new ListStreamsResultAdapter(result); 114 | List streamArns = extractStreamArns(result); 115 | assertEquals(streamArns, resultAdapter.getStreamNames()); 116 | } 117 | 118 | @Test 119 | public void testRealDataWithIds() { 120 | ListStreamsResult result = createResult(true); 121 | ListStreamsResultAdapter resultAdapter = new ListStreamsResultAdapter(result); 122 | assertEquals(extractStreamArns(result), resultAdapter.getStreamNames()); 123 | } 124 | 125 | private List extractStreamArns(ListStreamsResult result) { 126 | List streams = result.getStreams(); 127 | List streamArns = new ArrayList<>(streams.size()); 128 | for (Stream stream : streams) { 129 | streamArns.add(stream.getStreamArn()); 130 | } 131 | return streamArns; 132 | } 133 | 134 | private ListStreamsResult createResult(Boolean withArns) { 135 | java.util.List streams = new java.util.ArrayList<>(); 136 | if (withArns) { 137 | Stream stream = new Stream(); 138 | stream.setStreamArn(TEST_STRING); 139 | streams.add(stream); 140 | } 141 | return new ListStreamsResult().withStreams(streams).withLastEvaluatedStreamArn(TEST_STRING); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/RecordAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertNull; 10 | import static org.mockito.Mockito.times; 11 | import static org.mockito.Mockito.verify; 12 | import static org.powermock.api.mockito.PowerMockito.mock; 13 | import static org.powermock.api.mockito.PowerMockito.when; 14 | 15 | import java.io.IOException; 16 | import java.nio.ByteBuffer; 17 | import java.util.Date; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.UUID; 21 | 22 | import org.junit.Before; 23 | import org.junit.Test; 24 | import org.junit.runner.RunWith; 25 | import org.powermock.core.classloader.annotations.PrepareForTest; 26 | import org.powermock.modules.junit4.PowerMockRunner; 27 | import org.powermock.reflect.Whitebox; 28 | 29 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 30 | import com.amazonaws.services.dynamodbv2.model.OperationType; 31 | import com.amazonaws.services.dynamodbv2.model.Record; 32 | import com.amazonaws.services.dynamodbv2.model.StreamRecord; 33 | import com.amazonaws.services.dynamodbv2.model.StreamViewType; 34 | import com.fasterxml.jackson.core.JsonParseException; 35 | import com.fasterxml.jackson.core.JsonProcessingException; 36 | import com.fasterxml.jackson.databind.JsonMappingException; 37 | import com.fasterxml.jackson.databind.ObjectMapper; 38 | 39 | @PrepareForTest({ObjectMapper.class, RecordAdapter.class}) 40 | @RunWith(PowerMockRunner.class) 41 | public class RecordAdapterTest { 42 | 43 | private static final ObjectMapper MAPPER = new RecordObjectMapper(); 44 | 45 | private static final ObjectMapper MOCK_MAPPER = mock(RecordObjectMapper.class); 46 | 47 | private static final String TEST_STRING = "TestString"; 48 | 49 | private static final Date TEST_DATE = new Date(1156377600 /* EC2 Announced */); 50 | 51 | private static final String TEST_RECORD_v1_0 = 52 | new StringBuilder().append("{").append("\"awsRegion\":\"us-east-1\",").append("\"dynamodb\":").append("{").append("\"Keys\":").append("{") 53 | .append("\"hashKey\":{\"S\":\"hashKeyValue\"}").append("},").append("\"StreamViewType\":\"NEW_AND_OLD_IMAGES\",") 54 | .append("\"SequenceNumber\":\"100000000003498069978\",").append("\"SizeBytes\":6").append("},").append("\"eventID\":\"33fe21d365c03362c5e66d8dec2b63d5\",") 55 | .append("\"eventVersion\":\"1.0\",").append("\"eventName\":\"INSERT\",").append("\"eventSource\":\"aws:dynamodb\"").append("}").toString(); 56 | 57 | private Record testRecord; 58 | 59 | private RecordAdapter adapter; 60 | 61 | @Before 62 | public void setUpTest() { 63 | testRecord = new Record(); 64 | testRecord.setAwsRegion("us-east-1"); 65 | testRecord.setEventID(UUID.randomUUID().toString()); 66 | testRecord.setEventSource("aws:dynamodb"); 67 | testRecord.setEventVersion("1.1"); 68 | testRecord.setEventName(OperationType.MODIFY); 69 | StreamRecord testStreamRecord = new StreamRecord(); 70 | testRecord.setDynamodb(testStreamRecord); 71 | Map key = new HashMap(); 72 | key.put("hashKey", new AttributeValue("hashKeyValue")); 73 | Map oldImage = new HashMap(key); 74 | Map newImage = new HashMap(key); 75 | newImage.put("newAttributeKey", new AttributeValue("someValue")); 76 | testStreamRecord.setApproximateCreationDateTime(TEST_DATE); 77 | testStreamRecord.setKeys(key); 78 | testStreamRecord.setOldImage(oldImage); 79 | testStreamRecord.setNewImage(newImage); 80 | testStreamRecord.setSizeBytes(Long.MAX_VALUE); 81 | testStreamRecord.setSequenceNumber(UUID.randomUUID().toString()); 82 | testStreamRecord.setStreamViewType(StreamViewType.NEW_AND_OLD_IMAGES); 83 | testStreamRecord.setSequenceNumber(TEST_STRING); 84 | adapter = new RecordAdapter(testRecord); 85 | } 86 | 87 | @Test 88 | public void testDoesNotGenerateBytesWhenGenerateDataBytesIsFalse() { 89 | adapter = new RecordAdapter(testRecord, false); 90 | assertEquals(0, adapter.getData().array().length); 91 | } 92 | 93 | @Test 94 | public void testGetSequenceNumber() { 95 | String actual = adapter.getSequenceNumber(); 96 | assertEquals(TEST_STRING, actual); 97 | } 98 | 99 | @Test(expected = UnsupportedOperationException.class) 100 | public void testSetSequenceNumber() { 101 | adapter.setSequenceNumber(TEST_STRING); 102 | } 103 | 104 | @Test(expected = UnsupportedOperationException.class) 105 | public void testWithSequenceNumber() { 106 | adapter.withSequenceNumber(TEST_STRING); 107 | } 108 | 109 | @Test 110 | public void testGetData() throws JsonProcessingException { 111 | Whitebox.setInternalState(RecordAdapter.class, ObjectMapper.class, MOCK_MAPPER); 112 | when(MOCK_MAPPER.writeValueAsString(adapter.getInternalObject())).thenReturn(MAPPER.writeValueAsString(adapter.getInternalObject())); 113 | ByteBuffer data = ByteBuffer.wrap(MAPPER.writeValueAsString(adapter.getInternalObject()).getBytes()); 114 | assertEquals(data, adapter.getData()); 115 | // Retrieve data twice to validate it is only deserialized once 116 | assertEquals(data, adapter.getData()); 117 | verify(MOCK_MAPPER, times(1)).writeValueAsString(adapter.getInternalObject()); 118 | } 119 | 120 | @Test(expected = RuntimeException.class) 121 | public void testGetDataMappingException() throws JsonProcessingException { 122 | Whitebox.setInternalState(RecordAdapter.class, ObjectMapper.class, MOCK_MAPPER); 123 | when(MOCK_MAPPER.writeValueAsString(adapter.getInternalObject())).thenThrow(mock(JsonProcessingException.class)); 124 | adapter.getData(); 125 | } 126 | 127 | /** 128 | * We need a custom serializer/deserializer to be able to process Record object because of the conflicts that arise 129 | * with the standard jackson mapper for fields like eventName etc. 130 | */ 131 | @Test 132 | public void testGetDataDeserialized() throws JsonParseException, JsonMappingException, IOException { 133 | Whitebox.setInternalState(RecordAdapter.class, ObjectMapper.class, MAPPER); 134 | 135 | java.nio.ByteBuffer data = adapter.getData(); 136 | Record actual = MAPPER.readValue(data.array(), Record.class); 137 | assertEquals(adapter.getInternalObject(), actual); 138 | } 139 | 140 | @Test(expected = UnsupportedOperationException.class) 141 | public void testSetData() { 142 | adapter.setData(null); 143 | } 144 | 145 | @Test(expected = UnsupportedOperationException.class) 146 | public void testWithData() { 147 | adapter.withData(null); 148 | } 149 | 150 | @Test 151 | public void testGetPartitionKey() { 152 | assertEquals(adapter.getPartitionKey(), null); 153 | } 154 | 155 | @Test(expected = UnsupportedOperationException.class) 156 | public void testSetPartitionKey() { 157 | adapter.setPartitionKey(TEST_STRING); 158 | } 159 | 160 | @Test(expected = UnsupportedOperationException.class) 161 | public void testWithPartitionKey() { 162 | adapter.withPartitionKey(TEST_STRING); 163 | } 164 | 165 | @Test 166 | public void testApproximateCreationDateTime() throws IOException { 167 | String serialized = MAPPER.writeValueAsString(testRecord); 168 | Record deserialized = MAPPER.readValue(serialized, Record.class); 169 | com.amazonaws.services.kinesis.model.Record adapter = new RecordAdapter(deserialized); 170 | assertEquals(TEST_DATE, adapter.getApproximateArrivalTimestamp()); 171 | Date newDate = new Date(); 172 | adapter.setApproximateArrivalTimestamp(newDate); 173 | assertEquals(newDate, deserialized.getDynamodb().getApproximateCreationDateTime()); 174 | assertEquals(newDate, adapter.getApproximateArrivalTimestamp()); 175 | adapter.withApproximateArrivalTimestamp(TEST_DATE); 176 | assertEquals(TEST_DATE, deserialized.getDynamodb().getApproximateCreationDateTime()); 177 | assertEquals(TEST_DATE, adapter.getApproximateArrivalTimestamp()); 178 | } 179 | 180 | @Test 181 | public void testGetInternalObject() { 182 | com.amazonaws.services.kinesis.model.Record kinesisRecord = null; 183 | kinesisRecord = new RecordAdapter(testRecord); 184 | Record internalObject = ((RecordAdapter) kinesisRecord).getInternalObject(); 185 | assertEquals(testRecord, internalObject); 186 | } 187 | 188 | @Test 189 | public void testRecord_v1_0() throws IOException { 190 | Record deserialized = MAPPER.readValue(TEST_RECORD_v1_0, Record.class); 191 | com.amazonaws.services.kinesis.model.Record adapter = new RecordAdapter(deserialized); 192 | assertNull(adapter.getApproximateArrivalTimestamp()); 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/ShardAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.mockito.Mockito.when; 10 | 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.mockito.Mock; 14 | import org.mockito.MockitoAnnotations; 15 | 16 | import com.amazonaws.services.dynamodbv2.model.SequenceNumberRange; 17 | import com.amazonaws.services.dynamodbv2.model.Shard; 18 | 19 | public class ShardAdapterTest { 20 | private final String TEST_STRING = "TestString"; 21 | 22 | @Mock 23 | private Shard mockShard; 24 | 25 | @Mock 26 | private SequenceNumberRange mockSequenceNumberRange; 27 | 28 | private ShardAdapter adapter; 29 | 30 | @Before 31 | public void setUpTest() { 32 | MockitoAnnotations.initMocks(this); 33 | adapter = new ShardAdapter(mockShard); 34 | when(mockShard.getSequenceNumberRange()).thenReturn(mockSequenceNumberRange); 35 | } 36 | 37 | @Test 38 | public void testGetShardId() { 39 | when(mockShard.getShardId()).thenReturn(TEST_STRING); 40 | String actual = adapter.getShardId(); 41 | assertEquals(TEST_STRING, actual); 42 | } 43 | 44 | @Test(expected = UnsupportedOperationException.class) 45 | public void testSetShardId() { 46 | adapter.setShardId(TEST_STRING); 47 | } 48 | 49 | @Test(expected = UnsupportedOperationException.class) 50 | public void testWithShardId() { 51 | adapter.withShardId(TEST_STRING); 52 | } 53 | 54 | @Test 55 | public void testGetParentShardId() { 56 | when(mockShard.getParentShardId()).thenReturn(TEST_STRING); 57 | String actual = adapter.getParentShardId(); 58 | assertEquals(TEST_STRING, actual); 59 | } 60 | 61 | @Test(expected = UnsupportedOperationException.class) 62 | public void testSetParentShardId() { 63 | adapter.setParentShardId(TEST_STRING); 64 | } 65 | 66 | @Test(expected = UnsupportedOperationException.class) 67 | public void testWithParentShardId() { 68 | adapter.withParentShardId(TEST_STRING); 69 | } 70 | 71 | @Test 72 | public void testGetAdjacentParentShardId() { 73 | String actual = adapter.getAdjacentParentShardId(); 74 | assertEquals(null, actual); 75 | } 76 | 77 | @Test(expected = UnsupportedOperationException.class) 78 | public void testSetAdjacentParentShardId() { 79 | adapter.setAdjacentParentShardId(TEST_STRING); 80 | } 81 | 82 | @Test(expected = UnsupportedOperationException.class) 83 | public void testWithAdjacentParentShardId() { 84 | adapter.withAdjacentParentShardId(TEST_STRING); 85 | } 86 | 87 | @Test 88 | public void testGetHashKeyRange() { 89 | com.amazonaws.services.kinesis.model.HashKeyRange hashKeyRange = adapter.getHashKeyRange(); 90 | assertEquals(java.math.BigInteger.ZERO.toString(), hashKeyRange.getStartingHashKey()); 91 | assertEquals(java.math.BigInteger.ONE.toString(), hashKeyRange.getEndingHashKey()); 92 | } 93 | 94 | @Test(expected = UnsupportedOperationException.class) 95 | public void testSetHashKeyRange() { 96 | adapter.setHashKeyRange(null); 97 | } 98 | 99 | @Test(expected = UnsupportedOperationException.class) 100 | public void testWithHashKeyRange() { 101 | adapter.withHashKeyRange(null); 102 | } 103 | 104 | @Test 105 | public void testGetSequenceNumberRange() { 106 | when(mockSequenceNumberRange.getStartingSequenceNumber()).thenReturn(TEST_STRING); 107 | when(mockSequenceNumberRange.getEndingSequenceNumber()).thenReturn(TEST_STRING); 108 | com.amazonaws.services.kinesis.model.SequenceNumberRange sequenceNumberRange = adapter.getSequenceNumberRange(); 109 | assertEquals(TEST_STRING, sequenceNumberRange.getStartingSequenceNumber()); 110 | assertEquals(TEST_STRING, sequenceNumberRange.getEndingSequenceNumber()); 111 | } 112 | 113 | @Test(expected = UnsupportedOperationException.class) 114 | public void testSetSequenceNumberRange() { 115 | adapter.setSequenceNumberRange(null); 116 | } 117 | 118 | @Test(expected = UnsupportedOperationException.class) 119 | public void testWithSequenceNumberRange() { 120 | adapter.withSequenceNumberRange(null); 121 | } 122 | 123 | @Test 124 | public void testRealData() { 125 | Shard shard = createShard(); 126 | ShardAdapter shardAdapter = new ShardAdapter(shard); 127 | assertEquals(shard.getShardId(), shardAdapter.getShardId()); 128 | assertEquals(shard.getParentShardId(), shardAdapter.getParentShardId()); 129 | assertEquals(shard.getSequenceNumberRange().getStartingSequenceNumber(), shardAdapter.getSequenceNumberRange().getStartingSequenceNumber()); 130 | assertEquals(shard.getSequenceNumberRange().getEndingSequenceNumber(), shardAdapter.getSequenceNumberRange().getEndingSequenceNumber()); 131 | } 132 | 133 | private Shard createShard() { 134 | return new Shard().withShardId(TEST_STRING).withParentShardId(TEST_STRING) 135 | .withSequenceNumberRange(new SequenceNumberRange().withStartingSequenceNumber(TEST_STRING).withEndingSequenceNumber(TEST_STRING)); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/model/StreamDescriptionAdapterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.model; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertFalse; 10 | import static org.junit.Assert.assertSame; 11 | import static org.junit.Assert.assertTrue; 12 | import static org.mockito.Mockito.when; 13 | 14 | import java.util.Collection; 15 | import java.util.Collections; 16 | 17 | import org.junit.Before; 18 | import org.junit.Test; 19 | import org.mockito.Mock; 20 | import org.mockito.MockitoAnnotations; 21 | 22 | import com.amazonaws.services.dynamodbv2.model.Shard; 23 | import com.amazonaws.services.dynamodbv2.model.StreamDescription; 24 | import com.amazonaws.services.dynamodbv2.model.StreamStatus; 25 | 26 | public class StreamDescriptionAdapterTest { 27 | private final String TEST_STRING = "TestString"; 28 | 29 | @Mock 30 | private StreamDescription mockDescription; 31 | 32 | private StreamDescriptionAdapter adapter; 33 | 34 | @Before 35 | public void setUpTest() { 36 | MockitoAnnotations.initMocks(this); 37 | java.util.List shards = new java.util.ArrayList(); 38 | shards.add(new Shard()); 39 | when(mockDescription.getShards()).thenReturn(shards); 40 | adapter = new StreamDescriptionAdapter(mockDescription); 41 | } 42 | 43 | @Test 44 | public void testGetStreamName() { 45 | when(mockDescription.getStreamArn()).thenReturn(TEST_STRING); 46 | String actual = adapter.getStreamName(); 47 | assertEquals(TEST_STRING, actual); 48 | } 49 | 50 | @Test(expected = UnsupportedOperationException.class) 51 | public void testSetStreamName() { 52 | adapter.setStreamName(TEST_STRING); 53 | } 54 | 55 | @Test(expected = UnsupportedOperationException.class) 56 | public void testWithStreamName() { 57 | adapter.withStreamName(TEST_STRING); 58 | } 59 | 60 | @Test 61 | public void testGetStreamARN() { 62 | when(mockDescription.getStreamArn()).thenReturn(TEST_STRING); 63 | String actual = adapter.getStreamARN(); 64 | assertEquals(TEST_STRING, actual); 65 | } 66 | 67 | @Test(expected = UnsupportedOperationException.class) 68 | public void testSetStreamARN() { 69 | adapter.setStreamARN(TEST_STRING); 70 | } 71 | 72 | @Test(expected = UnsupportedOperationException.class) 73 | public void testWithStreamARN() { 74 | adapter.withStreamARN(TEST_STRING); 75 | } 76 | 77 | @Test 78 | public void testGetStreamStatus() { 79 | when(mockDescription.getStreamStatus()).thenReturn(StreamStatus.ENABLING.toString()); 80 | String actual = adapter.getStreamStatus(); 81 | assertEquals(com.amazonaws.services.kinesis.model.StreamStatus.CREATING.toString(), actual); 82 | 83 | when(mockDescription.getStreamStatus()).thenReturn(StreamStatus.ENABLED.toString()); 84 | actual = adapter.getStreamStatus(); 85 | assertEquals(com.amazonaws.services.kinesis.model.StreamStatus.ACTIVE.toString(), actual); 86 | 87 | when(mockDescription.getStreamStatus()).thenReturn(StreamStatus.DISABLING.toString()); 88 | actual = adapter.getStreamStatus(); 89 | assertEquals(com.amazonaws.services.kinesis.model.StreamStatus.ACTIVE.toString(), actual); 90 | 91 | when(mockDescription.getStreamStatus()).thenReturn(StreamStatus.DISABLED.toString()); 92 | actual = adapter.getStreamStatus(); 93 | assertEquals(com.amazonaws.services.kinesis.model.StreamStatus.ACTIVE.toString(), actual); 94 | } 95 | 96 | @Test(expected = UnsupportedOperationException.class) 97 | public void testUnsupportedStreamStatus() { 98 | when(mockDescription.getStreamStatus()).thenReturn(TEST_STRING); 99 | String actual = adapter.getStreamStatus(); 100 | } 101 | 102 | @Test(expected = UnsupportedOperationException.class) 103 | public void testSetStreamStatusFailure() { 104 | adapter.setStreamStatus(TEST_STRING); 105 | } 106 | 107 | @Test(expected = UnsupportedOperationException.class) 108 | public void testSetStreamStatusAsType() { 109 | adapter.setStreamStatus(com.amazonaws.services.kinesis.model.StreamStatus.CREATING); 110 | } 111 | 112 | @Test(expected = UnsupportedOperationException.class) 113 | public void testWithStreamStatusFailure() { 114 | adapter.withStreamStatus(TEST_STRING); 115 | } 116 | 117 | @Test(expected = UnsupportedOperationException.class) 118 | public void testWithStreamStatusAsType() { 119 | adapter.withStreamStatus(com.amazonaws.services.kinesis.model.StreamStatus.ACTIVE); 120 | } 121 | 122 | @Test 123 | public void testGetShardsWithItem() { 124 | java.util.List shardList = adapter.getShards(); 125 | assertEquals(1, shardList.size()); 126 | assertTrue(shardList.get(0) instanceof ShardAdapter); 127 | } 128 | 129 | @Test 130 | public void testGetShardsWithNoItems() { 131 | when(mockDescription.getShards()).thenReturn(new java.util.ArrayList()); 132 | StreamDescriptionAdapter localAdapter = new StreamDescriptionAdapter(mockDescription); 133 | java.util.List shardList = localAdapter.getShards(); 134 | assertTrue(shardList.isEmpty()); 135 | } 136 | 137 | @Test(expected = UnsupportedOperationException.class) 138 | public void testSetShards() { 139 | adapter.setShards(null); 140 | } 141 | 142 | @Test(expected = UnsupportedOperationException.class) 143 | public void testWithShards() { 144 | adapter.withShards(null, null); 145 | } 146 | 147 | @Test(expected = UnsupportedOperationException.class) 148 | public void testWithShards2() { 149 | final Collection shards = Collections.emptyList(); 150 | adapter.withShards(shards); 151 | } 152 | 153 | @Test 154 | public void testIsHasMoreShardsTrue() { 155 | when(mockDescription.getLastEvaluatedShardId()).thenReturn(TEST_STRING); 156 | assertTrue(adapter.isHasMoreShards()); 157 | } 158 | 159 | @Test 160 | public void testIsHasMoreShardsFalse() { 161 | when(mockDescription.getLastEvaluatedShardId()).thenReturn(null); 162 | assertFalse(adapter.isHasMoreShards()); 163 | } 164 | 165 | @Test 166 | public void testGetHasMoreShardsTrue() { 167 | when(mockDescription.getLastEvaluatedShardId()).thenReturn(TEST_STRING); 168 | assertTrue(adapter.getHasMoreShards()); 169 | } 170 | 171 | @Test 172 | public void testGetHasMoreShardsFalse() { 173 | when(mockDescription.getLastEvaluatedShardId()).thenReturn(null); 174 | assertFalse(adapter.getHasMoreShards()); 175 | } 176 | 177 | @Test(expected = UnsupportedOperationException.class) 178 | public void testSetHasMoreShards() { 179 | adapter.setHasMoreShards(false); 180 | } 181 | 182 | @Test(expected = UnsupportedOperationException.class) 183 | public void testWithHasMoreShards() { 184 | adapter.withHasMoreShards(true); 185 | } 186 | 187 | @Test 188 | public void testRealDataNoShards() { 189 | StreamDescription stream = createStreamDescription(false); 190 | StreamDescriptionAdapter streamAdapter = new StreamDescriptionAdapter(stream); 191 | assertEquals(stream.getStreamArn(), streamAdapter.getStreamName()); 192 | assertEquals(stream.getStreamArn(), streamAdapter.getStreamARN()); 193 | assertEquals(stream.getShards().size(), streamAdapter.getShards().size()); 194 | } 195 | 196 | @Test 197 | public void testRealDataWithShards() { 198 | StreamDescription stream = createStreamDescription(true); 199 | StreamDescriptionAdapter streamAdapter = new StreamDescriptionAdapter(stream); 200 | assertEquals(stream.getStreamArn(), streamAdapter.getStreamName()); 201 | assertEquals(stream.getStreamArn(), streamAdapter.getStreamARN()); 202 | assertEquals(stream.getShards().size(), streamAdapter.getShards().size()); 203 | } 204 | 205 | @Test 206 | public void testGetInternalObject() { 207 | com.amazonaws.services.kinesis.model.StreamDescription kinesisStreamDescription = new StreamDescriptionAdapter(mockDescription); 208 | StreamDescription internalObject = ((StreamDescriptionAdapter) kinesisStreamDescription).getInternalObject(); 209 | assertSame(mockDescription, internalObject); 210 | } 211 | 212 | private StreamDescription createStreamDescription(Boolean withShards) { 213 | java.util.List shards = new java.util.ArrayList(); 214 | if (withShards) { 215 | shards.add(new Shard()); 216 | } 217 | return new StreamDescription().withStreamArn(TEST_STRING).withShards(shards); 218 | } 219 | 220 | } 221 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/util/CountingRecordProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.util; 7 | 8 | import org.apache.commons.logging.Log; 9 | import org.apache.commons.logging.LogFactory; 10 | 11 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessor; 12 | import com.amazonaws.services.kinesis.clientlibrary.lib.worker.ShutdownReason; 13 | import com.amazonaws.services.kinesis.clientlibrary.types.InitializationInput; 14 | import com.amazonaws.services.kinesis.clientlibrary.types.ProcessRecordsInput; 15 | import com.amazonaws.services.kinesis.clientlibrary.types.ShutdownInput; 16 | import com.amazonaws.services.kinesis.model.Record; 17 | 18 | public class CountingRecordProcessor implements IRecordProcessor { 19 | 20 | private static final Log LOG = LogFactory.getLog(CountingRecordProcessor.class); 21 | 22 | private RecordProcessorTracker tracker; 23 | 24 | private String shardId; 25 | private Integer checkpointCounter; 26 | private Integer recordCounter; 27 | 28 | CountingRecordProcessor(RecordProcessorTracker tracker) { 29 | this.tracker = tracker; 30 | } 31 | 32 | @Override 33 | public void initialize(InitializationInput initializationInput) { 34 | this.shardId = initializationInput.getShardId(); 35 | checkpointCounter = 0; 36 | recordCounter = 0; 37 | } 38 | 39 | @Override 40 | public void processRecords(ProcessRecordsInput processRecordsInput) { 41 | for (Record record : processRecordsInput.getRecords()) { 42 | recordCounter += 1; 43 | checkpointCounter += 1; 44 | if (checkpointCounter % 10 == 0) { 45 | try { 46 | processRecordsInput.getCheckpointer().checkpoint(record.getSequenceNumber()); 47 | } catch (Exception e) { 48 | e.printStackTrace(); 49 | } 50 | } 51 | } 52 | } 53 | 54 | @Override 55 | public void shutdown(ShutdownInput shutdownInput) { 56 | if (shutdownInput.getShutdownReason() == ShutdownReason.TERMINATE) { 57 | try { 58 | shutdownInput.getCheckpointer().checkpoint(); 59 | } catch (Exception e) { 60 | e.printStackTrace(); 61 | } 62 | } 63 | LOG.info("Processed " + recordCounter + " records for " + shardId); 64 | tracker.shardProcessed(shardId, recordCounter); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/util/ExceptionThrowingLeaseManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.util; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import org.apache.commons.logging.Log; 12 | import org.apache.commons.logging.LogFactory; 13 | 14 | import com.amazonaws.services.kinesis.leases.exceptions.DependencyException; 15 | import com.amazonaws.services.kinesis.leases.exceptions.InvalidStateException; 16 | import com.amazonaws.services.kinesis.leases.exceptions.ProvisionedThroughputException; 17 | import com.amazonaws.services.kinesis.leases.impl.KinesisClientLease; 18 | import com.amazonaws.services.kinesis.leases.interfaces.ILeaseManager; 19 | 20 | /** 21 | * Mock Lease Manager by randomly throwing Leasing Exceptions. 22 | * Copied as-is from Kinesis Client Library. 23 | * 24 | */ 25 | public class ExceptionThrowingLeaseManager implements ILeaseManager { 26 | private static final Log LOG = LogFactory.getLog(ExceptionThrowingLeaseManager.class); 27 | private static final Throwable EXCEPTION_MSG = new Throwable("Test Exception"); 28 | 29 | // Use array below to control in what situations we want to throw exceptions. 30 | private int[] leaseManagerMethodCallingCount; 31 | 32 | /** 33 | * Methods which we support (simulate exceptions). 34 | */ 35 | public enum ExceptionThrowingLeaseManagerMethods { 36 | CREATELEASETABLEIFNOTEXISTS(0), 37 | LEASETABLEEXISTS(1), 38 | WAITUNTILLEASETABLEEXISTS(2), 39 | LISTLEASES(3), 40 | CREATELEASEIFNOTEXISTS(4), 41 | GETLEASE(5), 42 | RENEWLEASE(6), 43 | TAKELEASE(7), 44 | EVICTLEASE(8), 45 | DELETELEASE(9), 46 | DELETEALL(10), 47 | UPDATELEASE(11), 48 | NONE(Integer.MIN_VALUE); 49 | 50 | private Integer index; 51 | 52 | ExceptionThrowingLeaseManagerMethods(Integer index) { 53 | this.index = index; 54 | } 55 | 56 | Integer getIndex() { 57 | return this.index; 58 | } 59 | } 60 | 61 | // Define which method should throw exception and when it should throw exception. 62 | private ExceptionThrowingLeaseManagerMethods methodThrowingException = ExceptionThrowingLeaseManagerMethods.NONE; 63 | private int timeThrowingException = Integer.MAX_VALUE; 64 | 65 | // The real local lease manager which would do the real implementations. 66 | private final ILeaseManager leaseManager; 67 | 68 | /** 69 | * Constructor accepts lease manager as only argument. 70 | * 71 | * @param leaseManager which will do the real implementations 72 | */ 73 | public ExceptionThrowingLeaseManager(ILeaseManager leaseManager) { 74 | this.leaseManager = leaseManager; 75 | this.leaseManagerMethodCallingCount = new int[ExceptionThrowingLeaseManagerMethods.values().length]; 76 | } 77 | 78 | /** 79 | * Set parameters used for throwing exception. 80 | * 81 | * @param method which would throw exception 82 | * @param throwingTime defines what time to throw exception 83 | */ 84 | public void setLeaseLeaseManagerThrowingExceptionScenario(ExceptionThrowingLeaseManagerMethods method, int throwingTime) { 85 | this.methodThrowingException = method; 86 | this.timeThrowingException = throwingTime; 87 | } 88 | 89 | /** 90 | * Reset all parameters used for throwing exception. 91 | */ 92 | public void clearLeaseManagerThrowingExceptionScenario() { 93 | Arrays.fill(leaseManagerMethodCallingCount, 0); 94 | this.methodThrowingException = ExceptionThrowingLeaseManagerMethods.NONE; 95 | this.timeThrowingException = Integer.MAX_VALUE; 96 | } 97 | 98 | // Throw exception when the conditions are satisfied : 99 | // 1). method equals to methodThrowingException 100 | // 2). method calling count equals to what we want 101 | private void throwExceptions(String methodName, ExceptionThrowingLeaseManagerMethods method) 102 | throws DependencyException { 103 | // Increase calling count for this method 104 | leaseManagerMethodCallingCount[method.getIndex()]++; 105 | if (method.equals(methodThrowingException) 106 | && (leaseManagerMethodCallingCount[method.getIndex()] == timeThrowingException)) { 107 | // Throw Dependency Exception if all conditions are satisfied. 108 | LOG.debug("Throwing DependencyException in " + methodName); 109 | throw new DependencyException(EXCEPTION_MSG); 110 | } 111 | } 112 | 113 | @Override 114 | public boolean createLeaseTableIfNotExists(Long readCapacity, Long writeCapacity) 115 | throws ProvisionedThroughputException, DependencyException { 116 | throwExceptions("createLeaseTableIfNotExists", 117 | ExceptionThrowingLeaseManagerMethods.CREATELEASETABLEIFNOTEXISTS); 118 | 119 | return leaseManager.createLeaseTableIfNotExists(readCapacity, writeCapacity); 120 | } 121 | 122 | @Override 123 | public boolean leaseTableExists() throws DependencyException { 124 | throwExceptions("leaseTableExists", ExceptionThrowingLeaseManagerMethods.LEASETABLEEXISTS); 125 | 126 | return leaseManager.leaseTableExists(); 127 | } 128 | 129 | @Override 130 | public boolean waitUntilLeaseTableExists(long secondsBetweenPolls, long timeoutSeconds) throws DependencyException { 131 | throwExceptions("waitUntilLeaseTableExists", ExceptionThrowingLeaseManagerMethods.WAITUNTILLEASETABLEEXISTS); 132 | 133 | return leaseManager.waitUntilLeaseTableExists(secondsBetweenPolls, timeoutSeconds); 134 | } 135 | 136 | @Override 137 | public List listLeases() 138 | throws DependencyException, InvalidStateException, ProvisionedThroughputException { 139 | throwExceptions("listLeases", ExceptionThrowingLeaseManagerMethods.LISTLEASES); 140 | 141 | return leaseManager.listLeases(); 142 | } 143 | 144 | @Override 145 | public boolean createLeaseIfNotExists(KinesisClientLease lease) 146 | throws DependencyException, InvalidStateException, ProvisionedThroughputException { 147 | throwExceptions("createLeaseIfNotExists", ExceptionThrowingLeaseManagerMethods.CREATELEASEIFNOTEXISTS); 148 | 149 | return leaseManager.createLeaseIfNotExists(lease); 150 | } 151 | 152 | @Override 153 | public boolean renewLease(KinesisClientLease lease) 154 | throws DependencyException, InvalidStateException, ProvisionedThroughputException { 155 | throwExceptions("renewLease", ExceptionThrowingLeaseManagerMethods.RENEWLEASE); 156 | 157 | return leaseManager.renewLease(lease); 158 | } 159 | 160 | @Override 161 | public boolean takeLease(KinesisClientLease lease, String owner) 162 | throws DependencyException, InvalidStateException, ProvisionedThroughputException { 163 | throwExceptions("takeLease", ExceptionThrowingLeaseManagerMethods.TAKELEASE); 164 | 165 | return leaseManager.takeLease(lease, owner); 166 | } 167 | 168 | @Override 169 | public boolean evictLease(KinesisClientLease lease) 170 | throws DependencyException, InvalidStateException, ProvisionedThroughputException { 171 | throwExceptions("evictLease", ExceptionThrowingLeaseManagerMethods.EVICTLEASE); 172 | 173 | return leaseManager.evictLease(lease); 174 | } 175 | 176 | @Override 177 | public void deleteLease(KinesisClientLease lease) 178 | throws DependencyException, InvalidStateException, ProvisionedThroughputException { 179 | throwExceptions("deleteLease", ExceptionThrowingLeaseManagerMethods.DELETELEASE); 180 | 181 | leaseManager.deleteLease(lease); 182 | } 183 | 184 | @Override 185 | public boolean updateLease(KinesisClientLease lease) 186 | throws DependencyException, InvalidStateException, ProvisionedThroughputException { 187 | throwExceptions("updateLease", ExceptionThrowingLeaseManagerMethods.UPDATELEASE); 188 | 189 | return leaseManager.updateLease(lease); 190 | } 191 | 192 | @Override 193 | public KinesisClientLease getLease(String shardId) 194 | throws DependencyException, InvalidStateException, ProvisionedThroughputException { 195 | throwExceptions("getLease", ExceptionThrowingLeaseManagerMethods.GETLEASE); 196 | 197 | return leaseManager.getLease(shardId); 198 | } 199 | 200 | @Override 201 | public void deleteAll() throws DependencyException, InvalidStateException, ProvisionedThroughputException { 202 | throwExceptions("deleteAll", ExceptionThrowingLeaseManagerMethods.DELETEALL); 203 | 204 | leaseManager.deleteAll(); 205 | } 206 | 207 | @Override 208 | public boolean isLeaseTableEmpty() throws DependencyException, 209 | InvalidStateException, ProvisionedThroughputException { 210 | return false; 211 | } 212 | 213 | } 214 | 215 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/util/RecordProcessorTracker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.util; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Set; 11 | 12 | /** 13 | * The RecordProcessorTracker keeps track of the number of records 14 | * per shard which have been successfully processed. 15 | */ 16 | public class RecordProcessorTracker { 17 | 18 | private Set shardIds; 19 | private volatile Map processedRecordCounts; 20 | 21 | /** 22 | * Constructor. 23 | * 24 | * @param shardIds The shards which are being processed 25 | */ 26 | public RecordProcessorTracker(Set shardIds) { 27 | this.shardIds = shardIds; 28 | processedRecordCounts = new HashMap(); 29 | } 30 | 31 | /** 32 | * Invoked by the IRecordProcessor::shutdown() method. 33 | * 34 | * @param shardId The shard ID 35 | * @param count The number of records which have been successfully processed 36 | */ 37 | public void shardProcessed(String shardId, Integer count) { 38 | processedRecordCounts.put(shardId, count); 39 | } 40 | 41 | /** 42 | * Determines if the initially specified shards have all been 43 | * completely processed. 44 | * 45 | * @return True if all shards have been processed, false otherwise 46 | */ 47 | public boolean isDoneProcessing() { 48 | Set processedShards; 49 | synchronized (processedRecordCounts) { 50 | processedShards = processedRecordCounts.keySet(); 51 | } 52 | return processedShards.equals(shardIds); 53 | } 54 | 55 | /** 56 | * Returns the number of successfully processed records for each shard. 57 | * 58 | * @return Number of records processed per shard 59 | */ 60 | public Map getProcessedRecordCounts() { 61 | return processedRecordCounts; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/util/ReplicatingRecordProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.util; 7 | 8 | import static com.amazonaws.services.kinesis.clientlibrary.lib.worker.ShutdownReason.TERMINATE; 9 | 10 | import java.nio.charset.Charset; 11 | 12 | import org.apache.commons.logging.Log; 13 | import org.apache.commons.logging.LogFactory; 14 | 15 | import com.amazonaws.services.dynamodbv2.streamsadapter.model.RecordAdapter; 16 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessor; 17 | import com.amazonaws.services.kinesis.clientlibrary.types.InitializationInput; 18 | import com.amazonaws.services.kinesis.clientlibrary.types.ProcessRecordsInput; 19 | import com.amazonaws.services.kinesis.clientlibrary.types.ShutdownInput; 20 | import com.amazonaws.services.kinesis.model.Record; 21 | 22 | public class ReplicatingRecordProcessor implements IRecordProcessor { 23 | 24 | private static final Log LOG = LogFactory.getLog(ReplicatingRecordProcessor.class); 25 | 26 | private com.amazonaws.services.dynamodbv2.AmazonDynamoDB dynamoDBClient; 27 | private String tableName; 28 | private Integer checkpointCounter = -1; 29 | private Integer processRecordsCallCounter; 30 | 31 | public static final int CHECKPOINT_BATCH_SIZE = 10; 32 | 33 | ReplicatingRecordProcessor(com.amazonaws.services.dynamodbv2.AmazonDynamoDB dynamoDBClient, String tableName) { 34 | this.dynamoDBClient = dynamoDBClient; 35 | this.tableName = tableName; 36 | } 37 | 38 | @Override 39 | public void initialize(InitializationInput initializationInput) { 40 | checkpointCounter = 0; 41 | processRecordsCallCounter = 0; 42 | } 43 | 44 | @Override 45 | public void processRecords(ProcessRecordsInput processRecordsInput) { 46 | processRecordsCallCounter++; 47 | for (Record record : processRecordsInput.getRecords()) { 48 | String data = new String(record.getData().array(), Charset.forName("UTF-8")); 49 | LOG.info("Got record: " + data); 50 | if (record instanceof RecordAdapter) { 51 | com.amazonaws.services.dynamodbv2.model.Record usRecord = ((RecordAdapter) record).getInternalObject(); 52 | switch (usRecord.getEventName()) { 53 | case "INSERT": 54 | case "MODIFY": 55 | TestUtil.putItem(dynamoDBClient, tableName, usRecord.getDynamodb().getNewImage()); 56 | break; 57 | case "REMOVE": 58 | TestUtil.deleteItem(dynamoDBClient, tableName, usRecord.getDynamodb().getKeys().get("Id").getN()); 59 | break; 60 | } 61 | } 62 | checkpointCounter += 1; 63 | if (checkpointCounter % CHECKPOINT_BATCH_SIZE == 0) { 64 | try { 65 | processRecordsInput.getCheckpointer().checkpoint(record.getSequenceNumber()); 66 | } catch (Exception e) { 67 | e.printStackTrace(); 68 | } 69 | } 70 | } 71 | 72 | } 73 | 74 | @Override 75 | public void shutdown(ShutdownInput shutdownInput) { 76 | if (TERMINATE.equals(shutdownInput.getShutdownReason())) { 77 | try { 78 | shutdownInput.getCheckpointer().checkpoint(); 79 | } catch (Exception e) { 80 | e.printStackTrace(); 81 | } 82 | } 83 | } 84 | 85 | int getNumRecordsProcessed() { 86 | return checkpointCounter; 87 | } 88 | 89 | int getNumProcessRecordsCalls() { 90 | return processRecordsCallCounter; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/util/ShardObjectHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.util; 7 | 8 | import java.math.BigInteger; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import com.amazonaws.services.kinesis.model.HashKeyRange; 13 | import com.amazonaws.services.kinesis.model.SequenceNumberRange; 14 | import com.amazonaws.services.kinesis.model.Shard; 15 | 16 | /** 17 | * Helper class to create Shard, SequenceRange and related objects. 18 | * Copied as-is from Kinesis Client Library. 19 | */ 20 | public class ShardObjectHelper { 21 | 22 | private static final int EXPONENT = 128; 23 | 24 | /** 25 | * Max value of a sequence number (2^128 -1). Useful for defining sequence number range for a shard. 26 | */ 27 | static final String MAX_SEQUENCE_NUMBER = new BigInteger("2").pow(EXPONENT).subtract(BigInteger.ONE).toString(); 28 | 29 | /** 30 | * Min value of a sequence number (0). Useful for defining sequence number range for a shard. 31 | */ 32 | static final String MIN_SEQUENCE_NUMBER = BigInteger.ZERO.toString(); 33 | 34 | /** 35 | * Max value of a hash key (2^128 -1). Useful for defining hash key range for a shard. 36 | */ 37 | public static final String MAX_HASH_KEY = new BigInteger("2").pow(EXPONENT).subtract(BigInteger.ONE).toString(); 38 | 39 | /** 40 | * Min value of a hash key (0). Useful for defining sequence number range for a shard. 41 | */ 42 | public static final String MIN_HASH_KEY = BigInteger.ZERO.toString(); 43 | 44 | /** 45 | * 46 | */ 47 | private ShardObjectHelper() { 48 | } 49 | 50 | 51 | /** Helper method to create a new shard object. 52 | * @param shardId 53 | * @param parentShardId 54 | * @param adjacentParentShardId 55 | * @param sequenceNumberRange 56 | * @return 57 | */ 58 | public static Shard newShard(String shardId, 59 | String parentShardId, 60 | String adjacentParentShardId, 61 | SequenceNumberRange sequenceNumberRange) { 62 | return newShard(shardId, parentShardId, adjacentParentShardId, sequenceNumberRange, null); 63 | } 64 | 65 | /** Helper method to create a new shard object. 66 | * @param shardId 67 | * @param parentShardId 68 | * @param adjacentParentShardId 69 | * @param sequenceNumberRange 70 | * @param hashKeyRange 71 | * @return 72 | */ 73 | public static Shard newShard(String shardId, 74 | String parentShardId, 75 | String adjacentParentShardId, 76 | SequenceNumberRange sequenceNumberRange, 77 | HashKeyRange hashKeyRange) { 78 | Shard shard = new Shard(); 79 | shard.setShardId(shardId); 80 | shard.setParentShardId(parentShardId); 81 | shard.setAdjacentParentShardId(adjacentParentShardId); 82 | shard.setSequenceNumberRange(sequenceNumberRange); 83 | shard.setHashKeyRange(hashKeyRange); 84 | 85 | return shard; 86 | } 87 | 88 | /** Helper method. 89 | * @param startingSequenceNumber 90 | * @param endingSequenceNumber 91 | * @return 92 | */ 93 | public static SequenceNumberRange newSequenceNumberRange(String startingSequenceNumber, String endingSequenceNumber) { 94 | SequenceNumberRange range = new SequenceNumberRange(); 95 | range.setStartingSequenceNumber(startingSequenceNumber); 96 | range.setEndingSequenceNumber(endingSequenceNumber); 97 | return range; 98 | } 99 | 100 | /** Helper method. 101 | * @param startingHashKey 102 | * @param endingHashKey 103 | * @return 104 | */ 105 | public static HashKeyRange newHashKeyRange(String startingHashKey, String endingHashKey) { 106 | HashKeyRange range = new HashKeyRange(); 107 | range.setStartingHashKey(startingHashKey); 108 | range.setEndingHashKey(endingHashKey); 109 | return range; 110 | } 111 | 112 | public static List getParentShardIds(Shard shard) { 113 | List parentShardIds = new ArrayList<>(2); 114 | if (shard.getAdjacentParentShardId() != null) { 115 | parentShardIds.add(shard.getAdjacentParentShardId()); 116 | } 117 | if (shard.getParentShardId() != null) { 118 | parentShardIds.add(shard.getParentShardId()); 119 | } 120 | return parentShardIds; 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/util/TestRecordProcessorFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.util; 7 | 8 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; 9 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessor; 10 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessorFactory; 11 | 12 | /** 13 | * This implementation of IRecordProcessorFactory creates a variety of 14 | * record processors for different testing purposes. The type of processor 15 | * to be created is determined by the constructor. 16 | */ 17 | public class TestRecordProcessorFactory implements IRecordProcessorFactory { 18 | 19 | /** 20 | * The types of record processors which can be created by this factory. 21 | */ 22 | private enum Processor { 23 | REPLICATING, COUNTING 24 | } 25 | 26 | 27 | private Processor requestedProcessor; 28 | private RecordProcessorTracker tracker; 29 | private IRecordProcessor createdProcessor = null; 30 | 31 | /** 32 | * Using this constructor will result in the createProcessor method 33 | * returning a CountingRecordProcessor. 34 | * 35 | * @param tracker RecordProcessorTracker to keep track of the number of 36 | * processed records per shard 37 | */ 38 | public TestRecordProcessorFactory(RecordProcessorTracker tracker) { 39 | this.tracker = tracker; 40 | requestedProcessor = Processor.COUNTING; 41 | } 42 | 43 | private String tableName; 44 | private com.amazonaws.services.dynamodbv2.AmazonDynamoDB dynamoDB; 45 | 46 | /** 47 | * Using this constructor will result in the createProcessor method 48 | * returning a ReplicatingRecordProcessor. 49 | * 50 | * @param credentials AWS credentials used to access DynamoDB 51 | * @param dynamoDBEndpoint DynamoDB endpoint 52 | * @param serviceName Used to initialize the DynamoDB client 53 | * @param tableName The name of the table used for replication 54 | */ 55 | public TestRecordProcessorFactory(com.amazonaws.auth.AWSCredentialsProvider credentials, String dynamoDBEndpoint, String serviceName, String tableName) { 56 | this.tableName = tableName; 57 | requestedProcessor = Processor.REPLICATING; 58 | 59 | this.dynamoDB = new AmazonDynamoDBClient(credentials); 60 | dynamoDB.setEndpoint(dynamoDBEndpoint); 61 | ((AmazonDynamoDBClient) dynamoDB).setServiceNameIntern(serviceName); 62 | } 63 | 64 | /** 65 | * Using this constructor creates a replicating processor for an 66 | * embedded(in-memory) instance of DynamoDB local 67 | * 68 | * @param dynamoDB DynamoDB client for embedded DynamoDB instance 69 | * @param tableName The name of the table used for replication 70 | */ 71 | public TestRecordProcessorFactory(com.amazonaws.services.dynamodbv2.AmazonDynamoDB dynamoDB, String tableName) { 72 | this.tableName = tableName; 73 | this.dynamoDB = dynamoDB; 74 | requestedProcessor = Processor.REPLICATING; 75 | } 76 | 77 | @Override 78 | public IRecordProcessor createProcessor() { 79 | switch (requestedProcessor) { 80 | case REPLICATING: 81 | createdProcessor = new ReplicatingRecordProcessor(dynamoDB, tableName); 82 | break; 83 | case COUNTING: 84 | createdProcessor = new CountingRecordProcessor(tracker); 85 | break; 86 | default: 87 | createdProcessor = new CountingRecordProcessor(tracker); 88 | break; 89 | } 90 | 91 | return createdProcessor; 92 | } 93 | 94 | /** 95 | * This method returns -1 under the following conditions: 96 | * 1. createProcessor() has not yet been called 97 | * 2. initialize() method on the ReplicatingRecordProcessor instance has not yet been called 98 | * 3. requestedProcessor is COUNTING 99 | * 100 | * @return number of records processed by processRecords 101 | */ 102 | public int getNumRecordsProcessed() { 103 | if (createdProcessor == null) 104 | return -1; 105 | switch (requestedProcessor) { 106 | case REPLICATING: 107 | return ((ReplicatingRecordProcessor) createdProcessor).getNumRecordsProcessed(); 108 | default: 109 | return -1; 110 | } 111 | } 112 | 113 | public int getNumProcessRecordsCalls() { 114 | if (createdProcessor == null) 115 | return -1; 116 | switch (requestedProcessor) { 117 | case REPLICATING: 118 | return ((ReplicatingRecordProcessor) createdProcessor).getNumProcessRecordsCalls(); 119 | default: 120 | return -1; 121 | } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/dynamodbv2/streamsadapter/util/TestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.dynamodbv2.streamsadapter.util; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; 13 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; 14 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDBStreamsClient; 15 | import com.amazonaws.services.dynamodbv2.model.AttributeAction; 16 | import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; 17 | import com.amazonaws.services.dynamodbv2.model.AttributeValue; 18 | import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; 19 | import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; 20 | import com.amazonaws.services.dynamodbv2.model.CreateTableResult; 21 | import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; 22 | import com.amazonaws.services.dynamodbv2.model.DescribeStreamRequest; 23 | import com.amazonaws.services.dynamodbv2.model.DescribeStreamResult; 24 | import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; 25 | import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; 26 | import com.amazonaws.services.dynamodbv2.model.GetRecordsRequest; 27 | import com.amazonaws.services.dynamodbv2.model.GetRecordsResult; 28 | import com.amazonaws.services.dynamodbv2.model.GetShardIteratorRequest; 29 | import com.amazonaws.services.dynamodbv2.model.GetShardIteratorResult; 30 | import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; 31 | import com.amazonaws.services.dynamodbv2.model.KeyType; 32 | import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; 33 | import com.amazonaws.services.dynamodbv2.model.PutItemRequest; 34 | import com.amazonaws.services.dynamodbv2.model.QueryRequest; 35 | import com.amazonaws.services.dynamodbv2.model.QueryResult; 36 | import com.amazonaws.services.dynamodbv2.model.ResourceInUseException; 37 | import com.amazonaws.services.dynamodbv2.model.ScanRequest; 38 | import com.amazonaws.services.dynamodbv2.model.ScanResult; 39 | import com.amazonaws.services.dynamodbv2.model.ShardIteratorType; 40 | import com.amazonaws.services.dynamodbv2.model.StreamDescription; 41 | import com.amazonaws.services.dynamodbv2.model.StreamSpecification; 42 | import com.amazonaws.services.dynamodbv2.model.StreamViewType; 43 | import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; 44 | import com.amazonaws.services.dynamodbv2.model.UpdateTableRequest; 45 | 46 | public class TestUtil { 47 | 48 | /** 49 | * @return StreamId 50 | */ 51 | public static String createTable(com.amazonaws.services.dynamodbv2.AmazonDynamoDB client, String tableName, Boolean withStream) { 52 | java.util.List attributeDefinitions = new ArrayList(); 53 | attributeDefinitions.add(new AttributeDefinition().withAttributeName("Id").withAttributeType("N")); 54 | 55 | java.util.List keySchema = new ArrayList(); 56 | keySchema.add(new KeySchemaElement().withAttributeName("Id").withKeyType(KeyType.HASH)); 57 | 58 | ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput().withReadCapacityUnits(20L).withWriteCapacityUnits(20L); 59 | 60 | CreateTableRequest createTableRequest = new CreateTableRequest().withTableName(tableName).withAttributeDefinitions(attributeDefinitions).withKeySchema(keySchema) 61 | .withProvisionedThroughput(provisionedThroughput); 62 | 63 | if (withStream) { 64 | StreamSpecification streamSpecification = new StreamSpecification(); 65 | streamSpecification.setStreamEnabled(true); 66 | streamSpecification.setStreamViewType(StreamViewType.NEW_IMAGE); 67 | createTableRequest.setStreamSpecification(streamSpecification); 68 | } 69 | 70 | try { 71 | CreateTableResult result = client.createTable(createTableRequest); 72 | return result.getTableDescription().getLatestStreamArn(); 73 | } catch (ResourceInUseException e) { 74 | return describeTable(client, tableName).getTable().getLatestStreamArn(); 75 | } 76 | } 77 | 78 | public static void waitForTableActive(AmazonDynamoDB client, String tableName) throws IllegalStateException { 79 | Integer retries = 0; 80 | Boolean created = false; 81 | while (!created && retries < 100) { 82 | DescribeTableResult result = describeTable(client, tableName); 83 | created = result.getTable().getTableStatus().equals("ACTIVE"); 84 | if (created) { 85 | return; 86 | } else { 87 | retries++; 88 | try { 89 | Thread.sleep(500 + 100 * retries); 90 | } catch (InterruptedException e) { 91 | // do nothing 92 | } 93 | } 94 | } 95 | throw new IllegalStateException("Table not active!"); 96 | } 97 | 98 | public static void updateTable(AmazonDynamoDBClient client, String tableName, Long readCapacity, Long writeCapacity) { 99 | ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput().withReadCapacityUnits(readCapacity).withWriteCapacityUnits(writeCapacity); 100 | 101 | UpdateTableRequest updateTableRequest = new UpdateTableRequest().withTableName(tableName).withProvisionedThroughput(provisionedThroughput); 102 | 103 | client.updateTable(updateTableRequest); 104 | } 105 | 106 | public static DescribeTableResult describeTable(AmazonDynamoDB client, String tableName) { 107 | return client.describeTable(new DescribeTableRequest().withTableName(tableName)); 108 | } 109 | 110 | public static StreamDescription describeStream(AmazonDynamoDBStreamsClient client, String streamArn, String lastEvaluatedShardId) { 111 | DescribeStreamResult result = client.describeStream(new DescribeStreamRequest().withStreamArn(streamArn).withExclusiveStartShardId(lastEvaluatedShardId)); 112 | return result.getStreamDescription(); 113 | } 114 | 115 | public static ScanResult scanTable(AmazonDynamoDB client, String tableName) { 116 | return client.scan(new ScanRequest().withTableName(tableName)); 117 | } 118 | 119 | public static QueryResult queryTable(AmazonDynamoDB client, String tableName, String partitionKey) { 120 | Map expressionAttributeValues = new HashMap(); 121 | expressionAttributeValues.put(":v_id", new AttributeValue().withN(partitionKey)); 122 | 123 | QueryRequest queryRequest = new QueryRequest().withTableName(tableName).withKeyConditionExpression("Id = :v_id").withExpressionAttributeValues(expressionAttributeValues); 124 | 125 | return client.query(queryRequest); 126 | } 127 | 128 | public static void putItem(AmazonDynamoDB client, String tableName, String id, String val) { 129 | java.util.Map item = new HashMap(); 130 | item.put("Id", new AttributeValue().withN(id)); 131 | item.put("attribute-1", new AttributeValue().withS(val)); 132 | 133 | PutItemRequest putItemRequest = new PutItemRequest().withTableName(tableName).withItem(item); 134 | client.putItem(putItemRequest); 135 | } 136 | 137 | public static void putItem(AmazonDynamoDB client, String tableName, java.util.Map items) { 138 | PutItemRequest putItemRequest = new PutItemRequest().withTableName(tableName).withItem(items); 139 | client.putItem(putItemRequest); 140 | } 141 | 142 | public static void updateItem(AmazonDynamoDB client, String tableName, String id, String val) { 143 | java.util.Map key = new HashMap(); 144 | key.put("Id", new AttributeValue().withN(id)); 145 | 146 | Map attributeUpdates = new HashMap(); 147 | AttributeValueUpdate update = new AttributeValueUpdate().withAction(AttributeAction.PUT).withValue(new AttributeValue().withS(val)); 148 | attributeUpdates.put("attribute-2", update); 149 | 150 | UpdateItemRequest updateItemRequest = new UpdateItemRequest().withTableName(tableName).withKey(key).withAttributeUpdates(attributeUpdates); 151 | client.updateItem(updateItemRequest); 152 | } 153 | 154 | public static void deleteItem(AmazonDynamoDB client, String tableName, String id) { 155 | java.util.Map key = new HashMap(); 156 | key.put("Id", new AttributeValue().withN(id)); 157 | 158 | DeleteItemRequest deleteItemRequest = new DeleteItemRequest().withTableName(tableName).withKey(key); 159 | client.deleteItem(deleteItemRequest); 160 | } 161 | 162 | public static String getShardIterator(AmazonDynamoDBStreamsClient client, String streamArn, String shardId) { 163 | GetShardIteratorResult result = 164 | client.getShardIterator(new GetShardIteratorRequest().withStreamArn(streamArn).withShardId(shardId).withShardIteratorType(ShardIteratorType.TRIM_HORIZON)); 165 | return result.getShardIterator(); 166 | } 167 | 168 | public static GetRecordsResult getRecords(AmazonDynamoDBStreamsClient client, String shardIterator) { 169 | GetRecordsResult result = client.getRecords(new GetRecordsRequest().withShardIterator(shardIterator)); 170 | return result; 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/services/kinesis/clientlibrary/lib/worker/KinesisClientLibraryRecordDeserializationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package com.amazonaws.services.kinesis.clientlibrary.lib.worker; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertNotNull; 10 | import static org.junit.Assert.assertTrue; 11 | import static org.mockito.Matchers.any; 12 | import static org.mockito.Mockito.atMost; 13 | import static org.mockito.Mockito.verify; 14 | import static org.powermock.api.mockito.PowerMockito.mock; 15 | import static org.powermock.api.mockito.PowerMockito.when; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.Date; 20 | import java.util.List; 21 | 22 | import com.amazonaws.services.dynamodbv2.streamsadapter.DynamoDBStreamsDataFetcher; 23 | import com.amazonaws.services.dynamodbv2.streamsadapter.DynamoDBStreamsShutdownTask; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | import org.mockito.ArgumentCaptor; 27 | import org.powermock.core.classloader.annotations.PrepareForTest; 28 | import org.powermock.modules.junit4.PowerMockRunner; 29 | 30 | import com.amazonaws.auth.BasicAWSCredentials; 31 | import com.amazonaws.internal.StaticCredentialsProvider; 32 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDBStreams; 33 | import com.amazonaws.services.dynamodbv2.model.DescribeStreamRequest; 34 | import com.amazonaws.services.dynamodbv2.model.DescribeStreamResult; 35 | import com.amazonaws.services.dynamodbv2.model.GetRecordsRequest; 36 | import com.amazonaws.services.dynamodbv2.model.GetRecordsResult; 37 | import com.amazonaws.services.dynamodbv2.model.GetShardIteratorRequest; 38 | import com.amazonaws.services.dynamodbv2.model.GetShardIteratorResult; 39 | import com.amazonaws.services.dynamodbv2.model.SequenceNumberRange; 40 | import com.amazonaws.services.dynamodbv2.model.Shard; 41 | import com.amazonaws.services.dynamodbv2.model.StreamDescription; 42 | import com.amazonaws.services.dynamodbv2.model.StreamRecord; 43 | import com.amazonaws.services.dynamodbv2.model.StreamStatus; 44 | import com.amazonaws.services.dynamodbv2.streamsadapter.AmazonDynamoDBStreamsAdapterClient; 45 | import com.amazonaws.services.dynamodbv2.streamsadapter.model.RecordAdapter; 46 | import com.amazonaws.services.kinesis.clientlibrary.exceptions.KinesisClientLibException; 47 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.ICheckpoint; 48 | import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessor; 49 | import com.amazonaws.services.kinesis.clientlibrary.lib.checkpoint.Checkpoint; 50 | import com.amazonaws.services.kinesis.clientlibrary.proxies.IKinesisProxy; 51 | import com.amazonaws.services.kinesis.clientlibrary.proxies.KinesisProxyFactory; 52 | import com.amazonaws.services.kinesis.clientlibrary.types.ExtendedSequenceNumber; 53 | import com.amazonaws.services.kinesis.clientlibrary.types.InitializationInput; 54 | import com.amazonaws.services.kinesis.clientlibrary.types.ProcessRecordsInput; 55 | import com.amazonaws.services.kinesis.model.Record; 56 | 57 | @PrepareForTest({IKinesisProxy.class, KinesisDataFetcher.class}) 58 | @RunWith(PowerMockRunner.class) 59 | public class KinesisClientLibraryRecordDeserializationTests { 60 | 61 | /* Constants for mocking DynamoDB Streams */ 62 | private static final String STREAM_NAME = "stream-1"; 63 | private static final String SHARD_ID = "shard-000000"; 64 | private static final String SEQUENCE_NUMBER_0 = "0000000000000000"; 65 | private static final String SHARD_ITERATOR = "iterator-0000000000"; 66 | private static final Shard SHARD = new Shard().withShardId(SHARD_ID).withSequenceNumberRange(new SequenceNumberRange().withStartingSequenceNumber(SEQUENCE_NUMBER_0)); 67 | private static final StreamDescription STREAM_DESCRIPTION = 68 | new StreamDescription().withCreationRequestDateTime(new Date()).withKeySchema().withShards(SHARD).withStreamArn(STREAM_NAME).withStreamStatus(StreamStatus.ENABLED); 69 | private static final StreamRecord STREAM_RECORD_0 = new StreamRecord().withSequenceNumber(SEQUENCE_NUMBER_0).withApproximateCreationDateTime(new Date()); 70 | private static final com.amazonaws.services.dynamodbv2.model.Record RECORD_0 = new com.amazonaws.services.dynamodbv2.model.Record().withDynamodb(STREAM_RECORD_0); 71 | private static final List RECORDS = Arrays.asList(RECORD_0); 72 | 73 | /* Mocking the DynamoDB Streams client, Kinesis Client Library checkpoint interfaces, and Record Processor */ 74 | private static final AmazonDynamoDBStreams DYNAMODB_STREAMS = mock(AmazonDynamoDBStreams.class); 75 | private static final ICheckpoint CHECKPOINT = mock(ICheckpoint.class); 76 | private static final RecordProcessorCheckpointer CHECKPOINTER = mock(RecordProcessorCheckpointer.class); 77 | private static final IRecordProcessor RECORD_PROCESSOR = mock(IRecordProcessor.class); 78 | 79 | /* Construct higher level Kinesis Client Library objects from the primitive mocks */ 80 | private static final AmazonDynamoDBStreamsAdapterClient ADAPTER_CLIENT = new AmazonDynamoDBStreamsAdapterClient(DYNAMODB_STREAMS); 81 | private static final IKinesisProxy KINESIS_PROXY = 82 | new KinesisProxyFactory(new StaticCredentialsProvider(new BasicAWSCredentials("NotAnAccessKey", "NotASecretKey")), ADAPTER_CLIENT).getProxy(STREAM_NAME); 83 | private static final ShardInfo SHARD_INFO = new ShardInfo(SHARD_ID, "concurrencyToken", new ArrayList(), null /*checkpoint*/); 84 | private static final ExtendedSequenceNumber EXTENDED_SEQUENCE_NUMBER = new ExtendedSequenceNumber(SEQUENCE_NUMBER_0); 85 | private static final DynamoDBStreamsDataFetcher KINESIS_DATA_FETCHER = new DynamoDBStreamsDataFetcher(KINESIS_PROXY, SHARD_INFO); 86 | private static final StreamConfig STREAM_CONFIG = 87 | new StreamConfig(KINESIS_PROXY, 1000/* RecordLimit */, 0l /* IdleTimeMillis */, false /* callProcessRecordsForEmptyList */, false /* validateSequenceNumberBeforeCheckpointing */, 88 | InitialPositionInStreamExtended.newInitialPosition(InitialPositionInStream.TRIM_HORIZON)); 89 | private static final int GET_RECORDS_ITEM_LIMIT = 1000; 90 | private static final ExtendedSequenceNumber NULL_EXTENDED_SEQUENCE_NUMBER = null; 91 | 92 | @Test 93 | public void testVerifyKCLProvidesRecordAdapter() throws KinesisClientLibException { 94 | // Setup mocks 95 | when(CHECKPOINT.getCheckpointObject(SHARD_ID)).thenReturn(new Checkpoint(ExtendedSequenceNumber.TRIM_HORIZON, NULL_EXTENDED_SEQUENCE_NUMBER)); 96 | when(CHECKPOINTER.getLastCheckpointValue()).thenReturn(ExtendedSequenceNumber.TRIM_HORIZON); 97 | when(DYNAMODB_STREAMS.describeStream(any(DescribeStreamRequest.class))).thenReturn(new DescribeStreamResult().withStreamDescription(STREAM_DESCRIPTION)); 98 | when(DYNAMODB_STREAMS.getShardIterator(any(GetShardIteratorRequest.class))).thenReturn(new GetShardIteratorResult().withShardIterator(SHARD_ITERATOR)); 99 | when(DYNAMODB_STREAMS.getRecords(any(GetRecordsRequest.class))).thenReturn(new GetRecordsResult().withNextShardIterator(SHARD_ITERATOR).withRecords(RECORDS)); 100 | 101 | 102 | GetRecordsCache cache = new BlockingGetRecordsCache(GET_RECORDS_ITEM_LIMIT, new SynchronousGetRecordsRetrievalStrategy(KINESIS_DATA_FETCHER)); 103 | // Initialize the Record Processor 104 | InitializeTask initializeTask = new InitializeTask(SHARD_INFO, RECORD_PROCESSOR, CHECKPOINT, CHECKPOINTER, KINESIS_DATA_FETCHER, 0L /* backoffTimeMillis */, STREAM_CONFIG, cache); 105 | initializeTask.call(); 106 | // Execute process task 107 | ProcessTask processTask = new ProcessTask(SHARD_INFO, STREAM_CONFIG, RECORD_PROCESSOR, CHECKPOINTER, KINESIS_DATA_FETCHER, 0L /* backoffTimeMillis */, false /*skipShardSyncAtWorkerInitializationIfLeasesExist*/, cache); 108 | processTask.call(); 109 | 110 | // Verify mocks 111 | verify(CHECKPOINT).getCheckpointObject(SHARD_ID); 112 | verify(CHECKPOINTER).setLargestPermittedCheckpointValue(EXTENDED_SEQUENCE_NUMBER); 113 | verify(CHECKPOINTER).setInitialCheckpointValue(ExtendedSequenceNumber.TRIM_HORIZON); 114 | verify(CHECKPOINTER).getLastCheckpointValue(); 115 | verify(DYNAMODB_STREAMS, atMost(1)).describeStream(any(DescribeStreamRequest.class)); 116 | verify(DYNAMODB_STREAMS).getShardIterator(any(GetShardIteratorRequest.class)); 117 | verify(DYNAMODB_STREAMS).getRecords(any(GetRecordsRequest.class)); 118 | // Capture the input to the ProcessRecords method 119 | final ArgumentCaptor processRecordsInputCapture = ArgumentCaptor.forClass(ProcessRecordsInput.class); 120 | verify(RECORD_PROCESSOR).initialize(any(InitializationInput.class)); 121 | verify(RECORD_PROCESSOR).processRecords(processRecordsInputCapture.capture()); 122 | 123 | // Verify the Records are delivered to the Record Processor as RecordAdapter objects 124 | ProcessRecordsInput processRecordsInput = processRecordsInputCapture.getValue(); 125 | assertNotNull(processRecordsInput); 126 | assertNotNull(processRecordsInput.getRecords()); 127 | assertEquals(RECORDS.size(), processRecordsInput.getRecords().size()); 128 | for (Record record : processRecordsInput.getRecords()) { 129 | assertTrue("Kinesis Client Library is unwrapping the DynamoDB Streams Record Adapter", record instanceof RecordAdapter); 130 | } 131 | } 132 | } 133 | --------------------------------------------------------------------------------