Skip to content

Commit 81e290b

Browse files
committed
feat(lambda-event-sources): starting position timestamp for kafka
1 parent fccb006 commit 81e290b

File tree

3 files changed

+129
-2
lines changed

3 files changed

+129
-2
lines changed

packages/aws-cdk-lib/aws-lambda-event-sources/README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ behavior:
232232
* __onFailure__: In the event a record fails and consumes all retries, the record will be sent to SQS queue or SNS topic that is specified here
233233
* __parallelizationFactor__: The number of batches to concurrently process on each shard.
234234
* __retryAttempts__: The maximum number of times a record should be retried in the event of failure.
235-
* __startingPosition__: Will determine where to begin consumption. 'LATEST' will start at the most recent record and ignore all records that arrived prior to attaching the event source, 'TRIM_HORIZON' will start at the oldest record and ensure you process all available data, while 'AT_TIMESTAMP' will start reading records from a specified time stamp. Note that 'AT_TIMESTAMP' is only supported for Amazon Kinesis streams.
235+
* __startingPosition__: Will determine where to begin consumption. 'LATEST' will start at the most recent record and ignore all records that arrived prior to attaching the event source, 'TRIM_HORIZON' will start at the oldest record and ensure you process all available data, while 'AT_TIMESTAMP' will start reading records from a specified time stamp.
236236
* __startingPositionTimestamp__: The time stamp from which to start reading. Used in conjunction with __startingPosition__ when set to 'AT_TIMESTAMP'.
237237
* __tumblingWindow__: The duration in seconds of a processing window when using streams.
238238
* __enabled__: If the DynamoDB Streams event source mapping should be enabled. The default is true.
@@ -252,7 +252,14 @@ myFunction.addEventSource(new KinesisEventSource(stream, {
252252

253253
## Kafka
254254

255-
You can write Lambda functions to process data either from [Amazon MSK](https://docs.aws.amazon.com/lambda/latest/dg/with-msk.html) or a [self managed Kafka](https://docs.aws.amazon.com/lambda/latest/dg/kafka-smaa.html) cluster.
255+
You can write Lambda functions to process data either from [Amazon MSK](https://docs.aws.amazon.com/lambda/latest/dg/with-msk.html) or a [self-managed Kafka](https://docs.aws.amazon.com/lambda/latest/dg/kafka-smaa.html) cluster. The following parameters will impact to the polling behavior:
256+
257+
* __startingPosition__: Will determine where to begin consumption. 'LATEST' will start at the most recent record and ignore all records that arrived prior to attaching the event source, 'TRIM_HORIZON' will start at the oldest record and ensure you process all available data, while 'AT_TIMESTAMP' will start reading records from a specified time stamp.
258+
* __startingPositionTimestamp__: The time stamp from which to start reading. Used in conjunction with __startingPosition__ when set to 'AT_TIMESTAMP'.
259+
* __batchSize__: Determines how many records are buffered before invoking your lambda function - could impact your function's memory usage (if too high) and ability to keep up with incoming data velocity (if too low).
260+
* __maxBatchingWindow__: The maximum amount of time to gather records before invoking the lambda. This increases the likelihood of a full batch at the cost of possibly delaying processing.
261+
* __onFailure__: In the event a record fails and consumes all retries, the record will be sent to SQS queue or SNS topic that is specified here
262+
* __enabled__: If the Kafka event source mapping should be enabled. The default is true.
256263

257264
The following code sets up Amazon MSK as an event source for a lambda function. Credentials will need to be configured to access the
258265
MSK cluster, as described in [Username/Password authentication](https://docs.aws.amazon.com/msk/latest/developerguide/msk-password.html).

packages/aws-cdk-lib/aws-lambda-event-sources/lib/kafka.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ export interface KafkaEventSourceProps extends BaseStreamEventSourceProps {
5555
* @default - discarded records are ignored
5656
*/
5757
readonly onFailure?: lambda.IEventSourceDlq;
58+
59+
/**
60+
* The time from which to start reading, in Unix time seconds.
61+
*
62+
* @default - no timestamp
63+
*/
64+
readonly startingPositionTimestamp?: number;
5865
}
5966

6067
/**
@@ -148,6 +155,15 @@ export class ManagedKafkaEventSource extends StreamEventSource {
148155

149156
constructor(props: ManagedKafkaEventSourceProps) {
150157
super(props);
158+
159+
if (props.startingPosition === lambda.StartingPosition.AT_TIMESTAMP && !props.startingPositionTimestamp) {
160+
throw new Error('startingPositionTimestamp must be provided when startingPosition is AT_TIMESTAMP');
161+
}
162+
163+
if (props.startingPosition !== lambda.StartingPosition.AT_TIMESTAMP && props.startingPositionTimestamp) {
164+
throw new Error('startingPositionTimestamp can only be used when startingPosition is AT_TIMESTAMP');
165+
}
166+
151167
this.innerProps = props;
152168
}
153169

@@ -159,6 +175,7 @@ export class ManagedKafkaEventSource extends StreamEventSource {
159175
filters: this.innerProps.filters,
160176
filterEncryption: this.innerProps.filterEncryption,
161177
startingPosition: this.innerProps.startingPosition,
178+
startingPositionTimestamp: this.innerProps.startingPositionTimestamp,
162179
sourceAccessConfigurations: this.sourceAccessConfigurations(),
163180
kafkaTopic: this.innerProps.topic,
164181
kafkaConsumerGroupId: this.innerProps.consumerGroupId,
@@ -239,6 +256,15 @@ export class SelfManagedKafkaEventSource extends StreamEventSource {
239256
} else if (!props.secret) {
240257
throw new Error('secret must be set if Kafka brokers accessed over Internet');
241258
}
259+
260+
if (props.startingPosition === lambda.StartingPosition.AT_TIMESTAMP && !props.startingPositionTimestamp) {
261+
throw new Error('startingPositionTimestamp must be provided when startingPosition is AT_TIMESTAMP');
262+
}
263+
264+
if (props.startingPosition !== lambda.StartingPosition.AT_TIMESTAMP && props.startingPositionTimestamp) {
265+
throw new Error('startingPositionTimestamp can only be used when startingPosition is AT_TIMESTAMP');
266+
}
267+
242268
this.innerProps = props;
243269
}
244270

@@ -253,6 +279,7 @@ export class SelfManagedKafkaEventSource extends StreamEventSource {
253279
kafkaTopic: this.innerProps.topic,
254280
kafkaConsumerGroupId: this.innerProps.consumerGroupId,
255281
startingPosition: this.innerProps.startingPosition,
282+
startingPositionTimestamp: this.innerProps.startingPositionTimestamp,
256283
sourceAccessConfigurations: this.sourceAccessConfigurations(),
257284
onFailure: this.innerProps.onFailure,
258285
supportS3OnFailureDestination: true,

packages/aws-cdk-lib/aws-lambda-event-sources/test/kafka.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,48 @@ describe('KafkaEventSource', () => {
308308
});
309309
});
310310

311+
test('AT_TIMESTAMP starting position', () => {
312+
const stack = new cdk.Stack();
313+
const fn = new TestFunction(stack, 'Fn');
314+
const clusterArn = 'some-arn';
315+
const kafkaTopic = 'some-topic';
316+
317+
fn.addEventSource(new sources.ManagedKafkaEventSource({
318+
clusterArn,
319+
topic: kafkaTopic,
320+
startingPosition: lambda.StartingPosition.AT_TIMESTAMP,
321+
startingPositionTimestamp: 1640995200,
322+
}),
323+
);
324+
325+
Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', {
326+
StartingPosition: 'AT_TIMESTAMP',
327+
StartingPositionTimestamp: 1640995200,
328+
});
329+
});
330+
331+
test('startingPositionTimestamp missing throws error', () => {
332+
const clusterArn = 'some-arn';
333+
const kafkaTopic = 'some-topic';
334+
335+
expect(() => new sources.ManagedKafkaEventSource({
336+
clusterArn,
337+
topic: kafkaTopic,
338+
startingPosition: lambda.StartingPosition.AT_TIMESTAMP,
339+
})).toThrow(/startingPositionTimestamp must be provided when startingPosition is AT_TIMESTAMP/);
340+
});
341+
342+
test('startingPositionTimestamp without AT_TIMESTAMP throws error', () => {
343+
const clusterArn = 'some-arn';
344+
const kafkaTopic = 'some-topic';
345+
346+
expect(() => new sources.ManagedKafkaEventSource({
347+
clusterArn,
348+
topic: kafkaTopic,
349+
startingPosition: lambda.StartingPosition.LATEST,
350+
startingPositionTimestamp: 1640995200,
351+
})).toThrow(/startingPositionTimestamp can only be used when startingPosition is AT_TIMESTAMP/);
352+
});
311353
});
312354

313355
describe('self-managed kafka', () => {
@@ -998,5 +1040,56 @@ describe('KafkaEventSource', () => {
9981040
expect(mskEventMapping.eventSourceMappingId).toBeDefined();
9991041
expect(mskEventMapping.eventSourceMappingArn).toBeDefined();
10001042
});
1043+
1044+
test('AT_TIMESTAMP starting position', () => {
1045+
const stack = new cdk.Stack();
1046+
const fn = new TestFunction(stack, 'Fn');
1047+
const bootstrapServers = ['kafka-broker:9092'];
1048+
const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' });
1049+
const kafkaTopic = 'some-topic';
1050+
1051+
fn.addEventSource(new sources.SelfManagedKafkaEventSource({
1052+
bootstrapServers,
1053+
secret: secret,
1054+
topic: kafkaTopic,
1055+
startingPosition: lambda.StartingPosition.AT_TIMESTAMP,
1056+
startingPositionTimestamp: 1640995200,
1057+
}),
1058+
);
1059+
1060+
Template.fromStack(stack).hasResourceProperties('AWS::Lambda::EventSourceMapping', {
1061+
StartingPosition: 'AT_TIMESTAMP',
1062+
StartingPositionTimestamp: 1640995200,
1063+
});
1064+
});
1065+
1066+
test('startingPositionTimestamp missing throws error', () => {
1067+
const stack = new cdk.Stack();
1068+
const bootstrapServers = ['kafka-broker:9092'];
1069+
const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' });
1070+
const kafkaTopic = 'some-topic';
1071+
1072+
expect(() => new sources.SelfManagedKafkaEventSource({
1073+
bootstrapServers,
1074+
secret: secret,
1075+
topic: kafkaTopic,
1076+
startingPosition: lambda.StartingPosition.AT_TIMESTAMP,
1077+
})).toThrow(/startingPositionTimestamp must be provided when startingPosition is AT_TIMESTAMP/);
1078+
});
1079+
1080+
test('startingPositionTimestamp without AT_TIMESTAMP throws error', () => {
1081+
const stack = new cdk.Stack();
1082+
const bootstrapServers = ['kafka-broker:9092'];
1083+
const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' });
1084+
const kafkaTopic = 'some-topic';
1085+
1086+
expect(() => new sources.SelfManagedKafkaEventSource({
1087+
bootstrapServers,
1088+
secret: secret,
1089+
topic: kafkaTopic,
1090+
startingPosition: lambda.StartingPosition.LATEST,
1091+
startingPositionTimestamp: 1640995200,
1092+
})).toThrow(/startingPositionTimestamp can only be used when startingPosition is AT_TIMESTAMP/);
1093+
});
10011094
});
10021095
});

0 commit comments

Comments
 (0)