Skip to content
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

GH-249 Added properties for handling backpressure in KplMessageHandler #250

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
377dbe6
GH-249 Added properties for handling backpressure in KplMessageHandle…
siddharthjain210 Dec 8, 2024
3d7de0f
GH-249 Addressing code review comments related to code style.
siddharthjain210 Dec 14, 2024
4e3b571
GH-249 Addressed smart handling when KPL Buffer is at max capacity. I…
siddharthjain210 Dec 14, 2024
99e4d2d
GH-249 Code Review comments address. Updated Tests, javadocs. Added l…
siddharthjain210 Dec 21, 2024
512fa18
GH-249 Addressing consistency related comments in the javadocs.
siddharthjain210 Dec 21, 2024
af06a15
GH-249 Removing KPL Native Mentions in the java docs for KPL Message …
siddharthjain210 Dec 22, 2024
7daa438
GH-249 Handling the sleep duration inside the while loop.
siddharthjain210 Dec 22, 2024
35f0d9c
GH-249 Addressing code review comments, handling checkstyle failures.
siddharthjain210 Dec 22, 2024
9fb17f3
GH-249 Renamed maxInFlightRecordsInitBackoffDuration to maxInFlightRe…
siddharthjain210 Dec 22, 2024
fc96e7e
GH-249 Removing Blank lines from Java doc in KPL Message Handler.
siddharthjain210 Dec 24, 2024
3e6d178
GH-249 Using mockito mock method that doesnt require args,
siddharthjain210 Dec 27, 2024
33657f7
GH-249 Simplication of backpressure handling, introduced KPLBackpress…
siddharthjain210 Dec 29, 2024
4d4fcf8
GH-249 Javadoc corrections.
siddharthjain210 Dec 29, 2024
186314e
GH-249 Code Review comments addressed. Added Test cases with RetryAdv…
siddharthjain210 Dec 29, 2024
3c442c7
GH-249 Added Javadoc for KplBackpressureException in the KplMessageHa…
siddharthjain210 Dec 29, 2024
b7ae296
GH-249 Corrected all java docs in KplMessageHandler
siddharthjain210 Dec 29, 2024
25f98e6
Revert "GH-249 Corrected all java docs in KplMessageHandler"
siddharthjain210 Dec 31, 2024
974d0fc
GH-249 Javadoc related code review actions fixed.
siddharthjain210 Dec 31, 2024
51027aa
GH-249 Updated Copyright and Javadoc related comments.
siddharthjain210 Jan 2, 2025
12f7572
GH-249 Updated Copyright and Javadoc related comments. Revert in Abst…
siddharthjain210 Jan 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2024 the original author or authors.
* Copyright 2019-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -49,6 +49,7 @@
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.aws.support.AwsHeaders;
import org.springframework.integration.aws.support.KplBackpressureException;
import org.springframework.integration.aws.support.UserRecordResponse;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.integration.handler.AbstractMessageHandler;
Expand All @@ -63,11 +64,16 @@
import org.springframework.util.StringUtils;

/**
* The {@link AbstractMessageHandler} implementation for the Amazon Kinesis Producer
* Library {@code putRecord(s)}.
* The {@link AbstractMessageHandler} implementation for the Amazon Kinesis Producer Library {@code putRecord(s)}.
* <p>
* The {@link KplBackpressureException} is thrown when backpressure handling is enabled and buffer is at max capacity.
* This exception can be handled with
* {@link org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice}.
* </p>
*
* @author Arnaud Lecollaire
* @author Artem Bilan
* @author Siddharth Jain
*
* @since 2.2
*
Expand Down Expand Up @@ -99,6 +105,8 @@ public class KplMessageHandler extends AbstractAwsMessageHandler<Void> implement

private volatile ScheduledFuture<?> flushFuture;

private long backPressureThreshold = 0;

public KplMessageHandler(KinesisProducer kinesisProducer) {
Assert.notNull(kinesisProducer, "'kinesisProducer' must not be null.");
this.kinesisProducer = kinesisProducer;
Expand All @@ -115,6 +123,19 @@ public void setConverter(Converter<Object, byte[]> converter) {
setMessageConverter(new ConvertingFromMessageConverter(converter));
}

/**
* Configure maximum records in flight for handling backpressure. By default, backpressure handling is not enabled.
* When backpressure handling is enabled and number of records in flight exceeds the threshold, a
* {@link KplBackpressureException} would be thrown.
* @param backPressureThreshold Set a value greater than 0 to enable backpressure handling.
* @since 3.0.9
*/
public void setBackPressureThreshold(long backPressureThreshold) {
Assert.isTrue(backPressureThreshold >= 0,
"'backPressureThreshold must be greater than or equal to 0.");
this.backPressureThreshold = backPressureThreshold;
}

/**
* Configure a {@link MessageConverter} for converting payload to {@code byte[]} for Kinesis record.
* @param messageConverter the {@link MessageConverter} to use.
Expand Down Expand Up @@ -368,6 +389,14 @@ private void setGlueSchemaIntoUserRecordIfAny(UserRecord userRecord, Message<?>
}

private CompletableFuture<UserRecordResponse> handleUserRecord(UserRecord userRecord) {
if (this.backPressureThreshold > 0) {
var numberOfRecordsInFlight = this.kinesisProducer.getOutstandingRecordsCount();
if (numberOfRecordsInFlight > this.backPressureThreshold) {
throw new KplBackpressureException("Cannot send record to Kinesis since buffer is at max capacity.",
userRecord);
}
}

ListenableFuture<UserRecordResult> recordResult = this.kinesisProducer.addUserRecord(userRecord);
return listenableFutureToCompletableFuture(recordResult)
.thenApply(UserRecordResponse::new);
Expand Down Expand Up @@ -403,7 +432,8 @@ private PutRecordRequest buildPutRecordRequest(Message<?> message) {
if (!StringUtils.hasText(partitionKey) && this.partitionKeyExpression != null) {
partitionKey = this.partitionKeyExpression.getValue(getEvaluationContext(), message, String.class);
}
Assert.state(partitionKey != null, "'partitionKey' must not be null for sending a Kinesis record. "
Assert.state(partitionKey != null,
"'partitionKey' must not be null for sending a Kinesis record."
+ "Consider configuring this handler with a 'partitionKey'( or 'partitionKeyExpression') " +
"or supply an 'aws_partitionKey' message header.");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.integration.aws.support;

import com.amazonaws.services.kinesis.producer.UserRecord;

/**
* An exception triggered from {@link org.springframework.integration.aws.outbound.KplMessageHandler} while sending
* records to Kinesis when maximum number of records in flight exceeds the backpressure threshold.
*
* @author Siddharth Jain
*
* @since 3.0.9
*/
public class KplBackpressureException extends RuntimeException {

private static final long serialVersionUID = 1L;

private final UserRecord userRecord;

public KplBackpressureException(String message, UserRecord userRecord) {
super(message);
this.userRecord = userRecord;
}

/**
* Get the {@link UserRecord} related.
* @return {@link UserRecord} linked while sending the record to Kinesis.
*/
public UserRecord getUserRecord() {
return this.userRecord;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright 2019-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.integration.aws.outbound;

import com.amazonaws.services.kinesis.producer.KinesisProducer;
import com.amazonaws.services.kinesis.producer.UserRecord;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.aws.support.AwsHeaders;
import org.springframework.integration.aws.support.KplBackpressureException;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

/** The class contains test cases for KplMessageHandler.
*
* @author Siddharth Jain
*
* @since 3.0.9
*/
@SpringJUnitConfig
@DirtiesContext
public class KplMessageHandlerTests {

@Autowired
protected KinesisProducer kinesisProducer;

@Autowired
protected MessageChannel kinesisSendChannel;

@Autowired
protected KplMessageHandler kplMessageHandler;

@Test
@SuppressWarnings("unchecked")
void kplMessageHandlerWithRawPayloadBackpressureDisabledSuccess() {
given(this.kinesisProducer.addUserRecord(any(UserRecord.class)))
.willReturn(mock());
final Message<?> message = MessageBuilder
.withPayload("someMessage")
.setHeader(AwsHeaders.PARTITION_KEY, "somePartitionKey")
.setHeader(AwsHeaders.SEQUENCE_NUMBER, "10")
.setHeader("someHeaderKey", "someHeaderValue")
.build();


ArgumentCaptor<UserRecord> userRecordRequestArgumentCaptor = ArgumentCaptor
.forClass(UserRecord.class);
this.kplMessageHandler.setBackPressureThreshold(0);
this.kinesisSendChannel.send(message);
verify(this.kinesisProducer).addUserRecord(userRecordRequestArgumentCaptor.capture());
verify(this.kinesisProducer, Mockito.never()).getOutstandingRecordsCount();
UserRecord userRecord = userRecordRequestArgumentCaptor.getValue();
assertThat(userRecord.getStreamName()).isEqualTo("someStream");
assertThat(userRecord.getPartitionKey()).isEqualTo("somePartitionKey");
assertThat(userRecord.getExplicitHashKey()).isNull();
}

@Test
@SuppressWarnings("unchecked")
void kplMessageHandlerWithRawPayloadBackpressureEnabledCapacityAvailable() {
given(this.kinesisProducer.addUserRecord(any(UserRecord.class)))
.willReturn(mock());
this.kplMessageHandler.setBackPressureThreshold(2);
given(this.kinesisProducer.getOutstandingRecordsCount())
.willReturn(1);
final Message<?> message = MessageBuilder
.withPayload("someMessage")
.setHeader(AwsHeaders.PARTITION_KEY, "somePartitionKey")
.setHeader(AwsHeaders.SEQUENCE_NUMBER, "10")
.setHeader("someHeaderKey", "someHeaderValue")
.build();


ArgumentCaptor<UserRecord> userRecordRequestArgumentCaptor = ArgumentCaptor
.forClass(UserRecord.class);

this.kinesisSendChannel.send(message);
verify(this.kinesisProducer).addUserRecord(userRecordRequestArgumentCaptor.capture());
verify(this.kinesisProducer).getOutstandingRecordsCount();
UserRecord userRecord = userRecordRequestArgumentCaptor.getValue();
assertThat(userRecord.getStreamName()).isEqualTo("someStream");
assertThat(userRecord.getPartitionKey()).isEqualTo("somePartitionKey");
assertThat(userRecord.getExplicitHashKey()).isNull();
}

@Test
@SuppressWarnings("unchecked")
void kplMessageHandlerWithRawPayloadBackpressureEnabledCapacityInsufficient() {
given(this.kinesisProducer.addUserRecord(any(UserRecord.class)))
.willReturn(mock());
this.kplMessageHandler.setBackPressureThreshold(2);
given(this.kinesisProducer.getOutstandingRecordsCount())
.willReturn(5);
final Message<?> message = MessageBuilder
.withPayload("someMessage")
.setHeader(AwsHeaders.PARTITION_KEY, "somePartitionKey")
.setHeader(AwsHeaders.SEQUENCE_NUMBER, "10")
.setHeader("someHeaderKey", "someHeaderValue")
.build();

assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> this.kinesisSendChannel.send(message))
.withCauseInstanceOf(MessageHandlingException.class)
.withRootCauseExactlyInstanceOf(KplBackpressureException.class)
.withStackTraceContaining("Cannot send record to Kinesis since buffer is at max capacity.");

verify(this.kinesisProducer, Mockito.never()).addUserRecord(any(UserRecord.class));
verify(this.kinesisProducer).getOutstandingRecordsCount();
}

@AfterEach
public void tearDown() {
clearInvocations(this.kinesisProducer);
}

@Configuration
@EnableIntegration
public static class ContextConfiguration {

@Bean
public KinesisProducer kinesisProducer() {
return mock();
}

@Bean
public RequestHandlerRetryAdvice retryAdvice() {
RequestHandlerRetryAdvice requestHandlerRetryAdvice = new RequestHandlerRetryAdvice();
requestHandlerRetryAdvice.setRetryTemplate(RetryTemplate.builder()
.retryOn(KplBackpressureException.class)
.exponentialBackoff(100, 2.0, 1000)
.maxAttempts(3)
.build());
return requestHandlerRetryAdvice;
}

@Bean
@ServiceActivator(inputChannel = "kinesisSendChannel", adviceChain = {"retryAdvice"})
public MessageHandler kplMessageHandler(KinesisProducer kinesisProducer) {
KplMessageHandler kplMessageHandler = new KplMessageHandler(kinesisProducer);
kplMessageHandler.setAsync(true);
kplMessageHandler.setStream("someStream");
return kplMessageHandler;
}

}

}
Loading