-
Notifications
You must be signed in to change notification settings - Fork 202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support AWS Kinesis Data Streams as a Source #4836
Support AWS Kinesis Data Streams as a Source #4836
Conversation
81c887c
to
93ababc
Compare
...rg/opensearch/dataprepper/plugins/kinesis/extension/KinesisLeaseCoordinationTableConfig.java
Show resolved
Hide resolved
@@ -0,0 +1,50 @@ | |||
package org.opensearch.dataprepper.plugins.kinesis.source; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know we aren't following it consistently but I think it is better to name this as KinesisClientFactory.java. There are too many "ClientFactory.java" and it becomes search more difficult.
...org/opensearch/dataprepper/plugins/kinesis/source/configuration/AwsAuthenticationConfig.java
Outdated
Show resolved
Hide resolved
...java/org/opensearch/dataprepper/plugins/kinesis/source/processor/KinesisRecordProcessor.java
Outdated
Show resolved
Hide resolved
data-prepper-plugins/kinesis-source/src/test/resources/simple_pipeline_with_extensions.yaml
Show resolved
Hide resolved
1ca0077
to
09212fa
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sb2k16 , Thank you for this contribution. This initial PR review has a number of high-level comments.
.../org/opensearch/dataprepper/plugins/kinesis/extension/DefaultKinesisLeaseConfigSupplier.java
Outdated
Show resolved
Hide resolved
} | ||
} | ||
|
||
private void processRecord(KinesisClientRecord record, Consumer<Record<Event>> eventConsumer) throws IOException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should split the logic for processing an individual record out of this class to make the class easier to extend and test over time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sb2k16 , Let's make this a separate class. Perhaps KinesisRecordConverter
. It can be responsible for all the work for taking the record and using the codec. You can have some good tests on this as well.
We've had difficulty expanding our tests in other similar locations because we combine too many things in a single class.
|
||
// Checkpoint for shard | ||
if (kinesisStreamConfig.isEnableCheckPoint() && System.currentTimeMillis() - lastCheckpointTimeInMillis > checkpointIntervalMilliSeconds) { | ||
LOG.info("Regular checkpointing for shard " + kinesisShardId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LOG.info("Regular checkpointing for shard " + kinesisShardId); | |
LOG.debug("Regular checkpointing for shard {}", kinesisShardId); |
Use Sl4fj string interpolation.
Also, this is probably more of a debug log.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @dlvenable. I have fixed this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updating the checkpoint synchronously is a pattern that we should move away from, as it can lead to this issue (#4422). You can create a separate thread to do this checkpointing. To handle it for acknowledgments, you can add a progress check (example here: #4918). Unless KCL handles the ownership aspect and the checkpointing is just to show how far it has gone in a shard
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sbose2k21 , I believe you missed this one.
...s-source/src/main/java/org/opensearch/dataprepper/plugins/kinesis/source/KinesisService.java
Outdated
Show resolved
Hide resolved
7da25c7
to
ba2532d
Compare
this.awsAuthenticationConfig = awsAuthenticationConfig; | ||
} | ||
|
||
public DynamoDbAsyncClient buildDynamoDBClient(Region region) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it recommended to use the Async client? If so why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @graytaylor0 for your review. This is required to construct ConfigsBuilder class in KCL
} | ||
|
||
if (sourceConfig.getStreams() == null || sourceConfig.getStreams().isEmpty()) { | ||
throw new IllegalStateException("Streams are empty!"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should throw InvalidPluginConfiugrationException
here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's also make a clearer and less excited message.
No Kinesis streams provided.
|
||
// Checkpoint for shard | ||
if (kinesisStreamConfig.isEnableCheckPoint() && System.currentTimeMillis() - lastCheckpointTimeInMillis > checkpointIntervalMilliSeconds) { | ||
LOG.info("Regular checkpointing for shard " + kinesisShardId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updating the checkpoint synchronously is a pattern that we should move away from, as it can lead to this issue (#4422). You can create a separate thread to do this checkpointing. To handle it for acknowledgments, you can add a progress check (example here: #4918). Unless KCL handles the ownership aspect and the checkpointing is just to show how far it has gone in a shard
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @sb2k16 for this change! There are a few outstanding comments from the last review. And I have a few other comments as well.
|
||
@Getter | ||
@JsonProperty("enable_checkpoint") | ||
private boolean enableCheckPoint = DEFAULT_ENABLE_CHECKPOINT; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would somebody disable checkpointing? Also, the default is false
here. Why is that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since, we will be enabling acknowledgements
, this is set to false
by default.
// Checkpoint for shard | ||
if (kinesisStreamConfig.isEnableCheckPoint() && System.currentTimeMillis() - lastCheckpointTimeInMillis > checkpointInterval.toMillis()) { | ||
LOG.info("Regular checkpointing for shard " + kinesisShardId); | ||
checkpoint(processRecordsInput.checkpointer()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we should not call this when end-to-end acknowledgements are enabled. Otherwise, we will checkpoint prematurely.
return acknowledgementSetManager.create((result) -> { | ||
acknowledgementSetCallbackCounter.increment(); | ||
if (result) { | ||
LOG.info("acknowledgements received"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a debug
level log.
LOG.info("acknowledgements received"); | ||
checkpoint(processRecordsInput.checkpointer()); | ||
} else { | ||
LOG.info("acknowledgements received with false"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a debug
level log.
this.kinesisStreamConfig = getStreamConfig(kinesisSourceConfig); | ||
final PluginModel codecConfiguration = kinesisSourceConfig.getCodec(); | ||
final PluginSetting codecPluginSettings = new PluginSetting(codecConfiguration.getPluginName(), codecConfiguration.getPluginSettings()); | ||
this.codec = pluginFactory.loadPlugin(InputCodec.class, codecPluginSettings); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might KCL create multiple of these processors? We can create the InputCodec
once, either in KinesisService
(probably the ideal location) or KinesisSharedRecordProcessorFactory
. Then pass it in here. This way, multiple instances of KinesisRecordProcessor
all share one instance of InputCodec
.
This also prepares us to use #4838 once implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @dlvenable. I have passed the KinesisRecordConverter
object to each KinesisRecordProcessor
which is instantiated in KinesisSharedRecordProcessorFactory
.
kinesisRecordProcessor = new KinesisRecordProcessor(buffer, kinesisSourceConfig, acknowledgementSetManager, pluginMetrics, pluginFactory, streamIdentifier); | ||
kinesisRecordProcessor.initialize(initializationInput); | ||
|
||
Thread.sleep(2000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Awaitility instead of a sleep.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have modified the test to remove this.
kinesisRecordProcessor = new KinesisRecordProcessor(buffer, kinesisSourceConfig, acknowledgementSetManager, pluginMetrics, pluginFactory, streamIdentifier); | ||
kinesisRecordProcessor.initialize(initializationInput); | ||
|
||
Thread.sleep(2000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Awaitility instead of a sleep.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have modified the test to remove this.
kinesisRecordProcessor = new KinesisRecordProcessor(buffer, kinesisSourceConfig, acknowledgementSetManager, pluginMetrics, pluginFactory, streamIdentifier); | ||
kinesisRecordProcessor.initialize(initializationInput); | ||
|
||
Thread.sleep(2000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Awaitility instead of a sleep.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have modified the test to remove this.
kinesisRecordProcessor = new KinesisRecordProcessor(buffer, kinesisSourceConfig, acknowledgementSetManager, pluginMetrics, pluginFactory, streamIdentifier); | ||
kinesisRecordProcessor.initialize(initializationInput); | ||
|
||
Thread.sleep(2000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Awaitility instead of a sleep.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have modified the test to remove this.
@@ -0,0 +1,10 @@ | |||
package org.opensearch.dataprepper.plugins.kinesis.extension; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every class needs a license header. Please add to all files.
See: https://github.com/opensearch-project/data-prepper/blob/main/CONTRIBUTING.md#license-headers
Hint: If you use IntelliJ you can configure it to automatically add this. And you can have it add the headers to all the files in this Gradle module (since they are all new).
Signed-off-by: Souvik Bose <[email protected]>
Signed-off-by: Souvik Bose <[email protected]>
Signed-off-by: Souvik Bose <[email protected]>
Signed-off-by: Souvik Bose <[email protected]>
Signed-off-by: Souvik Bose <[email protected]>
Signed-off-by: Souvik Bose <[email protected]>
82aa510
to
57f5d42
Compare
Signed-off-by: Souvik Bose <[email protected]>
57f5d42
to
92a797b
Compare
|
||
@JsonProperty("acknowledgments") | ||
@Getter | ||
private boolean acknowledgments = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we start making acknowledgements as true
by default to all pull sources? If yes, we should start with this. @dlvenable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should keep the behavior consistent for now. I do think we should revisit in Data Prepper 3.0. It may make sense to have a disable_acknowledgements: true
in that case. But, let's do it consistently then.
this.recordProcessingErrors = pluginMetrics.counterWithTags(KINESIS_RECORD_PROCESSING_ERRORS, KINESIS_STREAM_TAG_KEY, streamIdentifier.streamName()); | ||
this.checkpointFailures = pluginMetrics.counterWithTags(KINESIS_CHECKPOINT_FAILURES, KINESIS_STREAM_TAG_KEY, streamIdentifier.streamName()); | ||
this.checkpointInterval = kinesisStreamConfig.getCheckPointInterval(); | ||
this.buffer = buffer; | ||
this.bufferAccumulator = bufferAccumulator; | ||
this.kinesisCheckpointerTracker = new KinesisCheckpointerTracker(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than include setKinesisCheckpointTracker
, just pass this in the constructor. Then you can provide the mocks in the test using that. You can then remove the setter as this can lead to code bugs down the line. And you can make the field final
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @dlvenable . I have removed the setter method and passed in the checkpoint tracker through the constructor.
checkpointerRecordList.get(extendedSequenceNumber).setReadyToCheckpoint(true); | ||
} | ||
|
||
public synchronized Optional<KinesisCheckpointerRecord> getLatestAvailableCheckpointRecord() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename this to popLatestAvailableCheckpointRecord
or something similar. It is mutating the structure and a get
method doesn't convey that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have renamed this method to popLatestReadyToCheckpointRecord
|
||
Optional<KinesisCheckpointerRecord> kinesisCheckpointerRecordOptional = kinesisCheckpointerTracker.getLatestAvailableCheckpointRecord(); | ||
if (kinesisCheckpointerRecordOptional.isPresent()) { | ||
RecordProcessorCheckpointer recordProcessorCheckpointer = kinesisCheckpointerRecordOptional.get().getCheckpointer(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you combine these lines with 201-205?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @dlvenable. I have refactored these lines and added a new method doCheckpoint
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @sb2k16 for this great contribution!
try { | ||
streamConfig = getStreamConfig(kinesisStreamConfig); | ||
} catch (Exception e) { | ||
throw new RuntimeException(e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What kind of exceptions get thrown here? If it's a config error we should throw InvalidPluginConfigurationException and provide context with the message where we can
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an internal StreamConfig
class which is passed to KCL. It is not data prepper kinesis stream config.
} | ||
|
||
private String getStreamIdentifierString(StreamDescription streamDescription) { | ||
String accountId = streamDescription.streamARN().split(COLON)[4]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this an actual ARN, or just an identifier? If Arn we should use the Arn
class to get the account id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @graytaylor0. I will fix this.
return new FormerStreamsLeasesDeletionStrategy.AutoDetectionAndDeferredDeletionStrategy() { | ||
@Override | ||
public Duration waitPeriodToDeleteFormerStreams() { | ||
return Duration.ofSeconds(10); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are trade offs for having this higher or lower?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is the strategy to perform cleaning up of the leases for streams that just has been deleted. The wait period is to allow for any pending processing of records before cleaning up the lease from the database.
this.applicationName = pipelineName; | ||
this.workerIdentifierGenerator = workerIdentifierGenerator; | ||
this.executorService = Executors.newFixedThreadPool(1); | ||
final PluginModel codecConfiguration = kinesisSourceConfig.getCodec(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this codec the type of data they send to Kinesis? It's not just JSON records from Kinesis streams? Or is it always provided from Kinesis as just bytes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we read from Kinesis, it always comes in as bytes. Reference Record
|
||
@Override | ||
public void leaseLost(LeaseLostInput leaseLostInput) { | ||
LOG.debug("Lease Lost"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this worth having a metric for or does kinesis already track it? Why does this happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
KCL already tracks this through a metric. This can happen if the current worker holding the lease is evicted by another worker which acquires the lease for the specific shard.
...org/opensearch/dataprepper/plugins/source/kinesis/configuration/AwsAuthenticationConfig.java
Outdated
Show resolved
Hide resolved
...java/org/opensearch/dataprepper/plugins/source/kinesis/processor/KinesisRecordProcessor.java
Outdated
Show resolved
Hide resolved
Signed-off-by: Souvik Bose <[email protected]>
4ded035
to
806f78d
Compare
Description
Implementation for a source plugin in data-prepper to support ingestion of Kinesis Data Streams. It uses KCL (Kinesls Client Library) under the hood for ingestion purposes.
KCL features used:
Pipeline configuration for source:
Issues Resolved
Resolves #1082
Check List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.