16
16
17
17
package org .springframework .integration .aws .kinesis ;
18
18
19
+ import java .util .Collections ;
19
20
import java .util .List ;
21
+ import java .util .Map ;
22
+ import java .util .concurrent .CompletableFuture ;
20
23
21
24
import org .junit .jupiter .api .AfterAll ;
22
25
import org .junit .jupiter .api .BeforeAll ;
23
26
import org .junit .jupiter .api .Test ;
24
27
import software .amazon .awssdk .core .SdkBytes ;
28
+ import software .amazon .awssdk .core .waiters .WaiterResponse ;
25
29
import software .amazon .awssdk .services .cloudwatch .CloudWatchAsyncClient ;
26
30
import software .amazon .awssdk .services .dynamodb .DynamoDbAsyncClient ;
31
+ import software .amazon .awssdk .services .dynamodb .model .AttributeDefinition ;
32
+ import software .amazon .awssdk .services .dynamodb .model .AttributeValue ;
33
+ import software .amazon .awssdk .services .dynamodb .model .CreateTableRequest ;
34
+ import software .amazon .awssdk .services .dynamodb .model .KeySchemaElement ;
35
+ import software .amazon .awssdk .services .dynamodb .model .KeyType ;
36
+ import software .amazon .awssdk .services .dynamodb .model .ProvisionedThroughput ;
37
+ import software .amazon .awssdk .services .dynamodb .model .PutItemRequest ;
38
+ import software .amazon .awssdk .services .dynamodb .model .PutItemResponse ;
39
+ import software .amazon .awssdk .services .dynamodb .model .ScalarAttributeType ;
27
40
import software .amazon .awssdk .services .kinesis .KinesisAsyncClient ;
28
41
import software .amazon .awssdk .services .kinesis .model .Consumer ;
42
+ import software .amazon .awssdk .services .kinesis .model .DescribeStreamResponse ;
29
43
import software .amazon .kinesis .common .InitialPositionInStream ;
30
44
import software .amazon .kinesis .common .InitialPositionInStreamExtended ;
31
45
import software .amazon .kinesis .metrics .MetricsFactory ;
37
51
import org .springframework .context .annotation .Configuration ;
38
52
import org .springframework .integration .IntegrationMessageHeaderAccessor ;
39
53
import org .springframework .integration .aws .LocalstackContainerTest ;
54
+ import org .springframework .integration .aws .inbound .kinesis .CheckpointMode ;
40
55
import org .springframework .integration .aws .inbound .kinesis .KclMessageDrivenChannelAdapter ;
56
+ import org .springframework .integration .aws .inbound .kinesis .ListenerMode ;
41
57
import org .springframework .integration .aws .support .AwsHeaders ;
42
58
import org .springframework .integration .channel .QueueChannel ;
43
59
import org .springframework .integration .config .EnableIntegration ;
53
69
* @author Artem Bilan
54
70
* @author Siddharth Jain
55
71
* @author Minkyu Moon
56
- *
57
72
* @since 3.0
58
73
*/
59
74
@ SpringJUnitConfig
60
75
@ DirtiesContext
61
76
public class KclMessageDrivenChannelAdapterTests implements LocalstackContainerTest {
62
77
63
78
private static final String TEST_STREAM = "TestStreamKcl" ;
79
+ public static final String LEASE_TABLE_NAME = "test_table" ;
80
+ public static final String TEST_DATA = "test data" ;
64
81
65
82
private static KinesisAsyncClient AMAZON_KINESIS ;
66
83
@@ -80,10 +97,10 @@ static void setup() {
80
97
DYNAMO_DB = LocalstackContainerTest .dynamoDbClient ();
81
98
CLOUD_WATCH = LocalstackContainerTest .cloudWatchClient ();
82
99
83
- AMAZON_KINESIS . createStream ( request -> request . streamName ( TEST_STREAM ). shardCount ( 1 ))
84
- . thenCompose ( result ->
85
- AMAZON_KINESIS . waiter (). waitUntilStreamExists ( request -> request . streamName ( TEST_STREAM )) )
86
- .join ();
100
+ CompletableFuture . allOf (
101
+ initialiseStream ( TEST_STREAM ),
102
+ initialiseLeaseTableFor ( LEASE_TABLE_NAME )
103
+ ) .join ();
87
104
}
88
105
89
106
@ AfterAll
@@ -96,20 +113,36 @@ static void tearDown() {
96
113
}
97
114
98
115
@ Test
99
- void kclChannelAdapterReceivesRecords () {
100
- String testData = "test data" ;
116
+ void kclChannelAdapterReceivesBatchedRecords () {
117
+ this .kclMessageDrivenChannelAdapter .setListenerMode (ListenerMode .batch );
118
+ this .kclMessageDrivenChannelAdapter .setCheckpointMode (CheckpointMode .batch );
119
+
120
+ Message <?> received = verifyRecordReceived (TEST_DATA );
121
+ assertThat (received .getPayload ()).isEqualTo (Collections .singletonList (TEST_DATA ));
122
+ List <?> receivedSequences = received .getHeaders ().get (AwsHeaders .RECEIVED_SEQUENCE_NUMBER , List .class );
123
+ assertThat (receivedSequences ).isNotEmpty ();
124
+ }
125
+
126
+ @ Test
127
+ void kclChannelAdapterReceivesSingleRecord () {
101
128
129
+ this .kclMessageDrivenChannelAdapter .setListenerMode (ListenerMode .record );
130
+ this .kclMessageDrivenChannelAdapter .setCheckpointMode (CheckpointMode .record );
131
+
132
+ Message <?> receive = verifyRecordReceived (TEST_DATA );
133
+ assertThat (receive .getPayload ()).isEqualTo (TEST_DATA );
134
+ assertThat (receive .getHeaders ()).containsKey (IntegrationMessageHeaderAccessor .SOURCE_DATA );
135
+ assertThat (receive .getHeaders ().get (AwsHeaders .RECEIVED_SEQUENCE_NUMBER , String .class )).isNotEmpty ();
136
+ }
137
+
138
+ private Message <?> verifyRecordReceived (String testData ) {
102
139
AMAZON_KINESIS .putRecord (request ->
103
140
request .streamName (TEST_STREAM )
104
141
.data (SdkBytes .fromUtf8String (testData ))
105
142
.partitionKey ("test" ));
106
143
107
- // We need so long delay because KCL has a more than a minute setup phase.
108
- Message <?> receive = this .kinesisReceiveChannel .receive (300_000 );
144
+ Message <?> receive = this .kinesisReceiveChannel .receive (5000 );
109
145
assertThat (receive ).isNotNull ();
110
- assertThat (receive .getPayload ()).isEqualTo (testData );
111
- assertThat (receive .getHeaders ()).containsKey (IntegrationMessageHeaderAccessor .SOURCE_DATA );
112
- assertThat (receive .getHeaders ().get (AwsHeaders .RECEIVED_SEQUENCE_NUMBER , String .class )).isNotEmpty ();
113
146
114
147
List <Consumer > streamConsumers =
115
148
AMAZON_KINESIS .describeStream (r -> r .streamName (TEST_STREAM ))
@@ -120,10 +153,11 @@ void kclChannelAdapterReceivesRecords() {
120
153
.consumers ();
121
154
122
155
// Because FanOut is false, there would be no Stream Consumers.
123
- assertThat (streamConsumers ).hasSize ( 0 );
156
+ assertThat (streamConsumers ).isEmpty ( );
124
157
125
158
List <String > tableNames = DYNAMO_DB .listTables ().join ().tableNames ();
126
- assertThat (tableNames ).contains ("test_table" );
159
+ assertThat (tableNames ).contains (LEASE_TABLE_NAME );
160
+ return receive ;
127
161
}
128
162
129
163
@ Test
@@ -176,10 +210,10 @@ public void pollingMaxRecordsIsPropagated() {
176
210
public static class TestConfiguration {
177
211
178
212
@ Bean
179
- public KclMessageDrivenChannelAdapter kclMessageDrivenChannelAdapter () {
213
+ public KclMessageDrivenChannelAdapter kclMessageDrivenChannelAdapter (PollableChannel kinesisReceiveChannel ) {
180
214
KclMessageDrivenChannelAdapter adapter =
181
215
new KclMessageDrivenChannelAdapter (AMAZON_KINESIS , CLOUD_WATCH , DYNAMO_DB , TEST_STREAM );
182
- adapter .setOutputChannel (kinesisReceiveChannel () );
216
+ adapter .setOutputChannel (kinesisReceiveChannel );
183
217
adapter .setStreamInitialSequence (
184
218
InitialPositionInStreamExtended .newInitialPosition (InitialPositionInStream .TRIM_HORIZON ));
185
219
adapter .setConverter (String ::new );
@@ -202,7 +236,45 @@ public KclMessageDrivenChannelAdapter kclMessageDrivenChannelAdapter() {
202
236
public PollableChannel kinesisReceiveChannel () {
203
237
return new QueueChannel ();
204
238
}
239
+ }
205
240
241
+ private static CompletableFuture <WaiterResponse <DescribeStreamResponse >> initialiseStream (String streamName ) {
242
+ return AMAZON_KINESIS .createStream (request -> request .streamName (streamName ).shardCount (1 ))
243
+ .thenCompose (
244
+ result -> AMAZON_KINESIS .waiter ().waitUntilStreamExists (request -> request .streamName (streamName )));
206
245
}
207
246
247
+ /**
248
+ * Initialises the lease table to improve KCL initialisation time
249
+ */
250
+ private static CompletableFuture <PutItemResponse > initialiseLeaseTableFor (String leaseTableName ) {
251
+ return DYNAMO_DB .createTable (CreateTableRequest .builder ()
252
+ .tableName (leaseTableName )
253
+ .attributeDefinitions (AttributeDefinition .builder ()
254
+ .attributeName ("leaseKey" )
255
+ .attributeType (ScalarAttributeType .S )
256
+ .build ())
257
+ .keySchema (KeySchemaElement .builder ()
258
+ .attributeName ("leaseKey" )
259
+ .keyType (KeyType .HASH )
260
+ .build ())
261
+ .provisionedThroughput (ProvisionedThroughput .builder ()
262
+ .readCapacityUnits (1L )
263
+ .writeCapacityUnits (1L )
264
+ .build ())
265
+ .build ())
266
+ .thenCompose (
267
+ result -> DYNAMO_DB .waiter ().waitUntilTableExists (request -> request .tableName (leaseTableName )))
268
+ .thenCompose (describeTableResponseWaiterResponse -> DYNAMO_DB .putItem (PutItemRequest .builder ()
269
+ .tableName (leaseTableName )
270
+ .item (Map .of (
271
+ "leaseKey" , AttributeValue .fromS ("shardId-000000000000" ),
272
+ "checkpoint" , AttributeValue .fromS ("TRIM_HORIZON" ),
273
+ "leaseCounter" , AttributeValue .fromN ("1" ),
274
+ "startingHashKey" , AttributeValue .fromS ("0" ),
275
+ "ownerSwitchesSinceCheckpoint" , AttributeValue .fromN ("0" ),
276
+ "checkpointSubSequenceNumber" , AttributeValue .fromN ("0" )
277
+ ))
278
+ .build ()));
279
+ }
208
280
}
0 commit comments