Why do requests interfere with one another requests and slow performance with Spring Integration during load testing? - spring-integration

What I am doing? I'm connecting to a remote server using TLSv1.2 and sending a max of 300 bytes of data and receive a response back also of the same size.
What is expected to deliver? During load testing we are expected to deliver 1000TPS.
Use at max using 50 Persistent TLS Connections.
What is going wrong?
During load testing, the max TPS we are receiving is 250TPS.
During load testing, we observed that the requests are interfering causing one request's response to get into another request response.
Configurations:
#EnableIntegration
#IntegrationComponentScan
#Configuration
public class TcpClientConfig implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
private final ConnectionProperty connectionProperty;
#Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
TcpClientConfig(ConnectionProperty connectionProperty) {
this.connectionProperty = connectionProperty;
}
#Bean
public AbstractClientConnectionFactory clientConnectionFactory() {
TcpNioClientConnectionFactory tcpNioClientConnectionFactory =
getTcpNioClientConnectionFactoryOf(
connectionProperty.getPrimaryHSMServerIpAddress(),
connectionProperty.getPrimaryHSMServerPort());
final List<AbstractClientConnectionFactory> fallBackConnections = getFallBackConnections();
fallBackConnections.add(tcpNioClientConnectionFactory);
final FailoverClientConnectionFactory failoverClientConnectionFactory =
new FailoverClientConnectionFactory(fallBackConnections);
return new CachingClientConnectionFactory(
failoverClientConnectionFactory, connectionProperty.getConnectionPoolSize());
}
#Bean
DefaultTcpNioSSLConnectionSupport connectionSupport() {
final DefaultTcpSSLContextSupport defaultTcpSSLContextSupport =
new DefaultTcpSSLContextSupport(
connectionProperty.getKeystorePath(),
connectionProperty.getTrustStorePath(),
connectionProperty.getKeystorePassword(),
connectionProperty.getTruststorePassword());
final String protocol = "TLSv1.2";
defaultTcpSSLContextSupport.setProtocol(protocol);
return new DefaultTcpNioSSLConnectionSupport(defaultTcpSSLContextSupport, false);
}
#Bean
public MessageChannel outboundChannel() {
return new DirectChannel();
}
#Bean
#ServiceActivator(inputChannel = "outboundChannel")
public MessageHandler outboundGateway(AbstractClientConnectionFactory clientConnectionFactory) {
TcpOutboundGateway tcpOutboundGateway = new TcpOutboundGateway();
tcpOutboundGateway.setConnectionFactory(clientConnectionFactory);
return tcpOutboundGateway;
}
#ServiceActivator(inputChannel = "error-channel")
public void handleError(ErrorMessage em) {
Throwable throwable = em.getPayload();
if(ExceptionUtils.indexOfThrowable(throwable, IOException.class)!=-1){
ExceptionHandler.throwHsmSystemTimeoutException();
}
throw new RuntimeException(throwable);
}
private List<AbstractClientConnectionFactory> getFallBackConnections() {
final int size = connectionProperty.getAdditionalHSMServersConfig().size();
List<AbstractClientConnectionFactory> collector = new ArrayList<>(size);
for (final Map.Entry<String, Integer> server :
connectionProperty.getAdditionalHSMServersConfig().entrySet()) {
collector.add(getTcpNioClientConnectionFactoryOf(server.getKey(), server.getValue()));
}
return collector;
}
private TcpNioClientConnectionFactory getTcpNioClientConnectionFactoryOf(
final String ipAddress, final int port) {
TcpNioClientConnectionFactory tcpNioClientConnectionFactory =
new TcpNioClientConnectionFactory(ipAddress, port);
tcpNioClientConnectionFactory.setUsingDirectBuffers(true);
tcpNioClientConnectionFactory.setDeserializer(new CustomDeserializer());
tcpNioClientConnectionFactory.setApplicationEventPublisher(applicationEventPublisher);
tcpNioClientConnectionFactory.setSoKeepAlive(true);
tcpNioClientConnectionFactory.setConnectTimeout(connectionProperty.getConnectionTimeout());
tcpNioClientConnectionFactory.setSoTcpNoDelay(true);
tcpNioClientConnectionFactory.setTcpNioConnectionSupport(connectionSupport());
return tcpNioClientConnectionFactory;
}
}
Deserializer
#Component
class CustomDeserializer extends DefaultDeserializer {
private static final int MAX_LENGTH = 80;
#Override
public Object deserialize(final InputStream inputStream) throws IOException {
StringBuilder stringBuffer = new StringBuilder(MAX_LENGTH);
int read = Integer.MIN_VALUE;
for (int loop = 0; loop < 300 && read != ']'; loop++) {
read = inputStream.read();
stringBuffer.append((char) read);
}
String reply = stringBuffer.toString();
return reply;
}
Gateway:
#Component
#MessagingGateway(defaultRequestChannel = "outboundChannel",errorChannel ="error-channel" )
public interface TcpClientGateway {
String send(String message);
}
Additional Information:
Our service is deployed on the GKE having the 8 to 10 Pods.
The target server is capable of handling more than 2000TPS we are licensed to use only 1000TPS.
Edit: Added startup logs:
{
"logger": "org.springframework.integration.config.DefaultConfiguringBeanFactoryPostProcessor",
"message": "No bean named \u0027errorChannel\u0027 has been explicitly defined. Therefore, a default PublishSubscribeChannel will be created.",
}
{
"logger": "org.springframework.integration.config.DefaultConfiguringBeanFactoryPostProcessor",
"message": "No bean named \u0027integrationHeaderChannelRegistry\u0027 has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created.",
}
{
"logger": "org.springframework.cloud.context.scope.GenericScope",
"message": "BeanFactory id\u003da426df04-fa05-397e-a849-44d732e3faa4",
}
{
"logger": "org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker",
"message": "Bean \u0027org.springframework.integration.config.IntegrationManagementConfiguration\u0027 of type [org.springframework.integration.config.IntegrationManagementConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)",
}
{
"logger": "org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker",
"message": "Bean \u0027integrationChannelResolver\u0027 of type [org.springframework.integration.support.channel.BeanFactoryChannelResolver] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)",
}
{
"logger": "org.apache.coyote.http11.Http11NioProtocol",
"message": "Initializing ProtocolHandler [\"http-nio-8080\"]",
}
{
"logger": "org.springframework.integration.endpoint.EventDrivenConsumer",
"message": "Adding {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the \u0027errorChannel\u0027 channel",
}
{
"logger": "org.springframework.integration.channel.PublishSubscribeChannel",
"message": "Channel \u0027decryptor-api.errorChannel\u0027 has 1 subscriber(s).",
}
{
"logger": "org.springframework.integration.endpoint.EventDrivenConsumer",
"message": "started bean \u0027_org.springframework.integration.errorLogger\u0027",
}
{
"logger": "org.springframework.integration.endpoint.EventDrivenConsumer",
"message": "Adding {service-activator:tcpClientConfig.handleError.serviceActivator} as a subscriber to the \u0027error-channel\u0027 channel",
}
{
"logger": "org.springframework.integration.channel.DirectChannel",
"message": "Channel \u0027decryptor-api.error-channel\u0027 has 1 subscriber(s).",
}
{
"logger": "org.springframework.integration.endpoint.EventDrivenConsumer",
"message": "started bean \u0027tcpClientConfig.handleError.serviceActivator\u0027",
}
{
"logger": "org.springframework.integration.endpoint.EventDrivenConsumer",
"message": "Adding {ip:tcp-outbound-gateway:tcpClientConfig.outboundGateway.serviceActivator} as a subscriber to the \u0027outboundChannel\u0027 channel",
}
{
"logger": "org.springframework.integration.channel.DirectChannel",
"message": "Channel \u0027decryptor-api.outboundChannel\u0027 has 1 subscriber(s).",
}
{
"logger": "org.springframework.integration.ip.tcp.connection.TcpNioClientConnectionFactory",
"message": "started org.springframework.integration.ip.tcp.connection.TcpNioClientConnectionFactory#619b7436, host\u003d10.1.4.4, port\u003d9021",
}
{
"logger": "org.springframework.integration.ip.tcp.connection.TcpNioClientConnectionFactory",
"message": "started org.springframework.integration.ip.tcp.connection.TcpNioClientConnectionFactory#71c1ca1, host\u003d10.1.4.4, port\u003d9021",
}
{
"logger": "org.springframework.integration.ip.tcp.connection.FailoverClientConnectionFactory",
"message": "started org.springframework.integration.ip.tcp.connection.FailoverClientConnectionFactory#20cf3ab3, host\u003d, port\u003d0",
}
{
"logger": "org.springframework.integration.ip.tcp.connection.CachingClientConnectionFactory",
"message": "started bean \u0027clientConnectionFactory\u0027; defined in: \u0027class path resource [com/globalpay/enterprise/integrations/decrypter/configuration/TcpClientConfig.class]\u0027; from source: \u0027com.globalpay.enterprise.integrations.decrypter.configuration.TcpClientConfig.clientConnectionFactory()\u0027, host\u003d, port\u003d0",
}
{
"logger": "org.springframework.integration.endpoint.EventDrivenConsumer",
"message": "started bean \u0027tcpClientConfig.outboundGateway.serviceActivator\u0027",
}
{
"logger": "org.springframework.integration.gateway.GatewayProxyFactoryBean$MethodInvocationGateway",
"message": "started bean \u0027tcpClientGateway#send(String)\u0027",
}
{
"logger": "org.springframework.integration.gateway.GatewayProxyFactoryBean",
"message": "started bean \u0027tcpClientGateway\u0027",
}
{
"logger": "org.apache.coyote.http11.Http11NioProtocol",
"message": "Starting ProtocolHandler [\"http-nio-8080\"]",
}

See a singleUse option of the connection factory:
/**
* If true, sockets created by this factory will be used once.
* #param singleUse The singleUse to set.
*/
public void setSingleUse(boolean singleUse) {
And then see JavaDocs of the TcpOutboundGateway:
* TCP outbound gateway that uses a client connection factory. If the factory is configured
* for single-use connections, each request is sent on a new connection; if the factory does not use
* single use connections, each request is blocked until the previous response is received
* (or times out). Asynchronous requests/responses over the same connection are not
* supported - use a pair of outbound/inbound adapters for that use case.
I'm not sure what is that TPS, but it would be great to know what your server side is doing to be sure that correlation of the reply to the request is going to happen properly.

Related

Spring Integration Default Response for Jms inboundGateway

Seeing the below exception when trying to send a default constructed response for Jms inboundGateway exception from the downstream call. We are extracting the failedMessage headers from the ErrorMessage and then setting the constructed response as payload. The replyChannel headers is matching with the initially logged message header
2023-01-26 20:34:32,623 [mqGatewayListenerContainer-1] WARN o.s.m.c.GenericMessagingTemplate$TemporaryReplyChannel - be776858594e7c79 Reply message received but the receiving thread has exited due to an exception while sending the request message:
ErrorMessage [payload=org.springframework.messaging.MessageHandlingException: Failed to send or receive; nested exception is java.io.UncheckedIOException: java.net.SocketTimeoutException: Connect timed out, failedMessage=GenericMessage [payload=NOT_PRINTED, headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#2454562d, b3=xxxxxxxxxxxx, nativeHeaders={}, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#2454562d, sourceTransacted=false, jms_correlationId=ID:xxxxxxxxxx, id=xxxxxxxxxx, jms_expiration=36000, timestamp=1674750867614}]
Code:
return IntegrationFlows.from(Jms.inboundGateway(mqGatewayListenerContainer)
.defaultReplyQueueName(replyQueue)
.replyChannel(mqReplyChannel)
.errorChannel(appErrorChannel)
.replyTimeout(mqReplyTimeoutSeconds * 1000L))
// log
.log(DEBUG, m -> "Request Headers: " + m.getHeaders() + ", Message: " + m.getPayload())
// transform with required response headers
.transform(Message.class, m -> MessageBuilder.withPayload(m.getPayload())
.setHeader(ERROR_CHANNEL, m.getHeaders().get(ERROR_CHANNEL))
.setHeader(REPLY_CHANNEL, m.getHeaders().get(REPLY_CHANNEL))
.setHeader(CORRELATION_ID, m.getHeaders().get(MESSAGE_ID))
.setHeader(EXPIRATION, mqReplyTimeoutSeconds * 1000L)
.setHeader(MSG_HDR_SOURCE_TRANSACTED, transacted)
.build())
return IntegrationFlows.from(appErrorChannel())
.publishSubscribeChannel(
pubSubSpec -> pubSubSpec.subscribe(sf -> sf.channel(globalErrorChannel))
.<MessagingException, Message<MessagingException>>
transform(AppMessageUtil::getFailedMessageWithoutHeadersAsPayload)
.transform(p -> "Failure")
.get();
public static Message<MessagingException> getFailedMessageAsPayload(final MessagingException messagingException) {
var failedMessage = messagingException.getFailedMessage();
var failedMessageHeaders = Objects.isNull(failedMessage) ? null : failedMessage.getHeaders();
return MessageBuilder.withPayload(messagingException)
.copyHeaders(failedMessageHeaders)
.build();
}
Since you perform the processing of the request message on the same thread, it is blocked on a send and therefore we just re-throw an exception as is:
try {
doSend(channel, requestMessage, sendTimeout);
}
catch (RuntimeException ex) {
tempReplyChannel.setSendFailed(true);
throw ex;
}
And as you see we mark that tempReplyChannel as failed on a send operation.
So, the replyChannel header correlated with that mqReplyChannel is out of use. If you get rid of it at all, then everything is OK. But you also cannot reply back an Exception since the framework treats it as an error to re-throw back to the listener container:
if (errorFlowReply != null && errorFlowReply.getPayload() instanceof Throwable) {
rethrow((Throwable) errorFlowReply.getPayload(), "error flow returned an Error Message");
}
So, here is a solution:
#SpringBootApplication
public class So75249125Application {
public static void main(String[] args) {
SpringApplication.run(So75249125Application.class, args);
}
#Bean
IntegrationFlow jmsFlow(ConnectionFactory connectionFactory) {
return IntegrationFlow.from(Jms.inboundGateway(connectionFactory)
.requestDestination("testDestination")
.errorChannel("appErrorChannel"))
.transform(payload -> {
throw new RuntimeException("intentional");
})
.get();
}
#Bean
IntegrationFlow errorFlow() {
return IntegrationFlow.from("appErrorChannel")
.transform(So75249125Application::getFailedMessageAsPayload)
.get();
}
public static Message<String> getFailedMessageAsPayload(MessagingException messagingException) {
var failedMessage = messagingException.getFailedMessage();
var failedMessageHeaders = failedMessage.getHeaders();
return MessageBuilder.withPayload("failed")
.copyHeaders(failedMessageHeaders)
.build();
}
}
and unit test:
#SpringBootTest
class So75249125ApplicationTests {
#Autowired
JmsTemplate jmsTemplate;
#Test
void errorFlowRepliesCorrectly() throws JMSException {
Message reply = this.jmsTemplate.sendAndReceive("testDestination", session -> session.createTextMessage("test"));
assertThat(reply.getBody(String.class)).isEqualTo("failed");
}
}
Or even better like this:
public static String getFailedMessageAsPayload(MessagingException messagingException) {
var failedMessage = messagingException.getFailedMessage();
return "Request for '" + failedMessage.getPayload() + "' has failed";
}
and this test:
#Test
void errorFlowRepliesCorrectly() throws JMSException {
String testData = "test";
Message reply = this.jmsTemplate.sendAndReceive("testDestination", session -> session.createTextMessage(testData));
assertThat(reply.getBody(String.class)).isEqualTo("Request for '" + testData + "' has failed");
}

How to poll for multiple files at once with Spring Integration with WebFlux?

I have the following configuration below for file monitoring using Spring Integration and WebFlux.
It works well, but if I drop in 100 files it will pick up one file at a time with a 10 second gap between the "Received a notification of new file" log messages.
How do I poll for multiple files at once, so I don't have to wait 1000 seconds for all my files to finally register?
#Configuration
#EnableIntegration
public class FileMonitoringConfig {
private static final Logger logger =
LoggerFactory.getLogger(FileMonitoringConfig.class.getName());
#Value("${monitoring.folder}")
private String monitoringFolder;
#Value("${monitoring.polling-in-seconds:10}")
private int pollingInSeconds;
#Bean
Publisher<Message<Object>> myMessagePublisher() {
return IntegrationFlows.from(
Files.inboundAdapter(new File(monitoringFolder))
.useWatchService(false),
e -> e.poller(Pollers.fixedDelay(pollingInSeconds, TimeUnit.SECONDS)))
.channel(myChannel())
.toReactivePublisher();
}
#Bean
Function<Flux<Message<Object>>, Publisher<Message<Object>>> myReactiveSource() {
return flux -> myMessagePublisher();
}
#Bean
FluxMessageChannel myChannel() {
return new FluxMessageChannel();
}
#Bean
#ServiceActivator(
inputChannel = "myChannel",
async = "true",
reactive = #Reactive("myReactiveSource"))
ReactiveMessageHandler myMessageHandler() {
return new ReactiveMessageHandler() {
#Override
public Mono<Void> handleMessage(Message<?> message) throws MessagingException {
return Mono.fromFuture(doHandle(message));
}
private CompletableFuture<Void> doHandle(Message<?> message) {
return CompletableFuture.runAsync(
() -> {
logger.info("Received a notification of new file: {}", message.getPayload());
File file = (File) message.getPayload();
});
}
};
}
}
The Inbound Channel Adapter polls a single data record from the source per poll cycle.
Consider to add maxMessagesPerPoll(-1) to your poller() configuration.
See more in docs: https://docs.spring.io/spring-integration/docs/current/reference/html/core.html#channel-adapter-namespace-inbound

An instance of EventSource with Guid 74af9f20-af6a-5582-9382-f21f674fb271 already exists

This trace message is popping up all over our AppInsights instance. I don't know what is means, or what could be the cause. I'm happy to provide any details that can help debug.
Trace message
AI (Internal): ERROR: Exception in Command Processing for EventSource Microsoft-ApplicationInsights-Core: An instance of EventSource with Guid 74af9f20-af6a-5582-9382-f21f674fb271 already exists.
sdkVersion : dotnet:2.7.2-23439
Code Sample :
[FunctionName("UpdateFunction")]
public async static Task Run(
[EventHubTrigger("Product-events", Connection = "EventHubConnectionString", ConsumerGroup = "%ConsumerGroupName%")]Message[] objMessage,
[Blob("%ProductBlobContainerName%", System.IO.FileAccess.Read, Connection = "BlobStorageConnectionString")] CloudBlobContainer productblobContainer,
[CosmosDB(databaseName: "DatabaseName", collectionName: "procudtCollectionName", ConnectionStringSetting = "DBConnectionString")]DocumentClient client,
ExecutionContext context, ILogger log)
{
TelemetryClient telemetry = TelemetryCreation.Instantiate(context);
ConcurrentBag<int> DocumentToInsert= new ConcurrentBag<int>();
ConcurrentBag<Message> DocumentToBeDeleted = new ConcurrentBag<Message>();
try
{
telemetry.TrackTrace($"{context.FunctionName} Started", SeverityLevel.Information, new Dictionary<string, string>
{
{
"PrefixName",
objMessage[0].ToString()
},
{
"NumberOfMsgs",
objMessage.Length.ToString()
}
});
await UpsertProvider(objMessage, productblobContainer, client, telemetry, DocumentToInsert, DocumentToBeDeleted);
if (DocumentToBeDeleted.Count > 0)
{
DeleteProviders.DeleteProvider(DocumentToBeDeleted.ToArray(), client, context, telemetry);
}
telemetry.TrackTrace($"{context.FunctionName} Completed", SeverityLevel.Information);
}
catch (Exception ex)
{
telemetry.TrackException(ex, new Dictionary<string, string> {
{ "Azure Background Function Name",context.FunctionName },
{ "Read Data from container > folder", productblobContainer.ToString() + ">" + objMessage[0].PrefixName.ToString()},
{ "Total Documents received", objMessage.Length.ToString() },
{ "Number of documents to insert", DocumentToInsert.Count.ToString() },
{ "List of Documents to be updated", JsonConvert.SerializeObject(DocumentToInsert).ToString() },
{ "Number of documents skipped (Location is null)", DocumentToBeDeleted.Count.ToString() },
{ "List of Documents skipped", JsonConvert.SerializeObject(DocumentToBeDeleted).ToString() },
{ "Exception Message",ex.Message },
{ "Exception InnerMessage",ex.InnerException.ToString() }
});
throw;
}
}

How do I prevent ListenerExecutionFailedException: Listener threw exception

What do I need to do to prevent the following Exception which is presumably thrown by RabbitMQ.
org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException: Listener threw exception
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.wrapToListenerExecutionFailedExceptionIfNeeded(AbstractMessageListenerContainer.java:877)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:787)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:707)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$001(SimpleMessageListenerContainer.java:98)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$1.invokeListener(SimpleMessageListenerContainer.java:189)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.invokeListener(SimpleMessageListenerContainer.java:1236)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:688)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:1190)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:1174)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$1200(SimpleMessageListenerContainer.java:98)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1363)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.springframework.messaging.MessageDeliveryException: failed to send Message to channel 'amqpLaunchSpringBatchJobFlow.channel#0'; nested exception is jp.ixam_drive.batch.service.JobExecutionRuntimeException: Failed to start job with name ads-insights-import and parameters {accessToken=<ACCESS_TOKEN>, id=act_1234567890, classifier=stats, report_run_id=1482330625184792, job_request_id=32}
at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:449)
at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:373)
at org.springframework.messaging.core.GenericMessagingTemplate.doSend(GenericMessagingTemplate.java:115)
at org.springframework.messaging.core.GenericMessagingTemplate.doSend(GenericMessagingTemplate.java:45)
at org.springframework.messaging.core.AbstractMessageSendingTemplate.send(AbstractMessageSendingTemplate.java:105)
at org.springframework.integration.endpoint.MessageProducerSupport.sendMessage(MessageProducerSupport.java:171)
at org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter.access$400(AmqpInboundChannelAdapter.java:45)
at org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter$1.onMessage(AmqpInboundChannelAdapter.java:95)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:784)
... 10 common frames omitted
Caused by: jp.ixam_drive.batch.service.JobExecutionRuntimeException: Failed to start job with name ads-insights-import and parameters {accessToken=<ACCESS_TOKEN>, id=act_1234567890, classifier=stats, report_run_id=1482330625184792, job_request_id=32}
at jp.ixam_drive.facebook.SpringBatchLauncher.launchJob(SpringBatchLauncher.java:42)
at jp.ixam_drive.facebook.AmqpBatchLaunchIntegrationFlows.lambda$amqpLaunchSpringBatchJobFlow$1(AmqpBatchLaunchIntegrationFlows.java:71)
at org.springframework.integration.dispatcher.AbstractDispatcher.tryOptimizedDispatch(AbstractDispatcher.java:116)
at org.springframework.integration.dispatcher.UnicastingDispatcher.doDispatch(UnicastingDispatcher.java:148)
at org.springframework.integration.dispatcher.UnicastingDispatcher.dispatch(UnicastingDispatcher.java:121)
at org.springframework.integration.channel.AbstractSubscribableChannel.doSend(AbstractSubscribableChannel.java:89)
at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:423)
... 18 common frames omitted
Caused by: org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for parameters={accessToken=<ACCESS_TOKEN>, id=act_1234567890, classifier=stats, report_run_id=1482330625184792, job_request_id=32}. If you want to run this job again, change the parameters.
at org.springframework.batch.core.repository.support.SimpleJobRepository.createJobExecution(SimpleJobRepository.java:126)
at sun.reflect.GeneratedMethodAccessor193.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.batch.core.repository.support.AbstractJobRepositoryFactoryBean$1.invoke(AbstractJobRepositoryFactoryBean.java:172)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at com.sun.proxy.$Proxy125.createJobExecution(Unknown Source)
at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:125)
at jp.ixam_drive.batch.service.JobOperationsService.launch(JobOperationsService.java:64)
at jp.ixam_drive.facebook.SpringBatchLauncher.launchJob(SpringBatchLauncher.java:37)
... 24 common frames omitted
when I have 2 instances of Spring Boot application both of which run the following code in parallel to execute Spring Batch Jobs?
#Configuration
#Conditional(AmqpBatchLaunchCondition.class)
#Slf4j
public class AmqpAsyncAdsInsightsConfiguration {
#Autowired
ObjectMapper objectMapper;
#Value("${batch.launch.amqp.routing-keys.async-insights}")
String routingKey;
#Bean
public IntegrationFlow amqpOutboundAsyncAdsInsights(AmqpTemplate amqpTemplate) {
return IntegrationFlows.from("async_ads_insights")
.<JobParameters, byte[]>transform(SerializationUtils::serialize)
.handle(Amqp.outboundAdapter(amqpTemplate).routingKey(routingKey)).get();
}
#Bean
public IntegrationFlow amqpAdsInsightsAsyncJobRequestFlow(FacebookMarketingServiceProvider serviceProvider,
JobParametersToApiParametersTransformer transformer, ConnectionFactory connectionFactory) {
return IntegrationFlows.from(Amqp.inboundAdapter(connectionFactory, routingKey))
.<byte[], JobParameters>transform(SerializationUtils::deserialize)
.<JobParameters, ApiParameters>transform(transformer)
.<ApiParameters>handle((payload, header) -> {
String accessToken = (String) header.get("accessToken");
String id = (String) header.get("object_id");
FacebookMarketingApi api = serviceProvider.getApi(accessToken);
String reportRunId = api.asyncRequestOperations().getReportRunId(id, payload.toMap());
ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put("accessToken", accessToken);
objectNode.put("id", id);
objectNode.put("report_run_id", reportRunId);
objectNode.put("classifier", (String) header.get("classifier"));
objectNode.put("job_request_id", (Long) header.get("job_request_id"));
return serialize(objectNode);
}).channel("ad_report_run_polling_channel").get();
}
#SneakyThrows
private String serialize(JsonNode jsonNode) {
return objectMapper.writeValueAsString(jsonNode);
}
}
#Configuration
#Conditional(AmqpBatchLaunchCondition.class)
#Slf4j
public class AmqpBatchLaunchIntegrationFlows {
#Autowired
SpringBatchLauncher batchLauncher;
#Value("${batch.launch.amqp.routing-keys.job-launch}")
String routingKey;
#Bean(name = "batch_launch_channel")
public MessageChannel batchLaunchChannel() {
return MessageChannels.executor(Executors.newSingleThreadExecutor()).get();
}
#Bean
public IntegrationFlow amqpOutbound(AmqpTemplate amqpTemplate,
#Qualifier("batch_launch_channel") MessageChannel batchLaunchChannel) {
return IntegrationFlows.from(batchLaunchChannel)
.<JobParameters, byte[]>transform(SerializationUtils::serialize)
.handle(Amqp.outboundAdapter(amqpTemplate).routingKey(routingKey)).get();
}
#Bean
public IntegrationFlow amqpLaunchSpringBatchJobFlow(ConnectionFactory connectionFactory) {
return IntegrationFlows.from(Amqp.inboundAdapter(connectionFactory, routingKey))
.handle(message -> {
String jobName = (String) message.getHeaders().get("job_name");
byte[] bytes = (byte[]) message.getPayload();
JobParameters jobParameters = SerializationUtils.deserialize(bytes);
batchLauncher.launchJob(jobName, jobParameters);
}).get();
}
}
#Configuration
#Slf4j
public class AsyncAdsInsightsConfiguration {
#Value("${batch.core.pool.size}")
public Integer batchCorePoolSize;
#Value("${ixam_drive.facebook.api.ads-insights.async-poll-interval}")
public String asyncPollInterval;
#Autowired
ObjectMapper objectMapper;
#Autowired
private DataSource dataSource;
#Bean(name = "async_ads_insights")
public MessageChannel adsInsightsAsyncJobRequestChannel() {
return MessageChannels.direct().get();
}
#Bean(name = "ad_report_run_polling_channel")
public MessageChannel adReportRunPollingChannel() {
return MessageChannels.executor(Executors.newFixedThreadPool(batchCorePoolSize)).get();
}
#Bean
public IntegrationFlow adReportRunPollingLoopFlow(FacebookMarketingServiceProvider serviceProvider) {
return IntegrationFlows.from(adReportRunPollingChannel())
.<String>handle((payload, header) -> {
ObjectNode jsonNode = deserialize(payload);
String accessToken = jsonNode.get("accessToken").asText();
String reportRunId = jsonNode.get("report_run_id").asText();
try {
AdReportRun adReportRun = serviceProvider.getApi(accessToken)
.fetchObject(reportRunId, AdReportRun.class);
log.debug("ad_report_run: {}", adReportRun);
return jsonNode.set("ad_report_run", objectMapper.valueToTree(adReportRun));
} catch (Exception e) {
log.error("failed while polling for ad_report_run.id: {}", reportRunId);
throw new RuntimeException(e);
}
}).<JsonNode, Boolean>route(payload -> {
JsonNode adReportRun = payload.get("ad_report_run");
return adReportRun.get("async_percent_completion").asInt() == 100 &&
"Job Completed".equals(adReportRun.get("async_status").asText());
}, rs -> rs.subFlowMapping(true,
f -> f.transform(JsonNode.class,
source -> {
JobParametersBuilder jobParametersBuilder = new JobParametersBuilder();
jobParametersBuilder
.addString("accessToken", source.get("accessToken").asText());
jobParametersBuilder.addString("id", source.get("id").asText());
jobParametersBuilder
.addString("classifier", source.get("classifier").asText());
jobParametersBuilder
.addLong("report_run_id", source.get("report_run_id").asLong());
jobParametersBuilder
.addLong("job_request_id", source.get("job_request_id").asLong());
return jobParametersBuilder.toJobParameters();
}).channel("batch_launch_channel"))
.subFlowMapping(false,
f -> f.transform(JsonNode.class, this::serialize)
.<String>delay("delay", asyncPollInterval, c -> c.transactional()
.messageStore(jdbcMessageStore()))
.channel(adReportRunPollingChannel()))).get();
}
#SneakyThrows
private String serialize(JsonNode jsonNode) {
return objectMapper.writeValueAsString(jsonNode);
}
#SneakyThrows
private ObjectNode deserialize(String payload) {
return objectMapper.readerFor(ObjectNode.class).readValue(payload);
}
#Bean
public JdbcMessageStore jdbcMessageStore() {
JdbcMessageStore jdbcMessageStore = new JdbcMessageStore(dataSource);
return jdbcMessageStore;
}
#Bean
public JobParametersToApiParametersTransformer jobParametersToApiParametersTransformer() {
return new JobParametersToApiParametersTransformer() {
#Override
protected ApiParameters transform(JobParameters jobParameters) {
ApiParameters.ApiParametersBuilder builder = ApiParameters.builder();
MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
String level = jobParameters.getString("level");
if (!StringUtils.isEmpty(level)) {
multiValueMap.set("level", level);
}
String fields = jobParameters.getString("fields");
if (!StringUtils.isEmpty(fields)) {
multiValueMap.set("fields", fields);
}
String filter = jobParameters.getString("filter");
if (filter != null) {
try {
JsonNode jsonNode = objectMapper.readTree(filter);
if (jsonNode != null && jsonNode.isArray()) {
List<ApiFilteringParameters> filteringParametersList = new ArrayList<>();
List<ApiSingleValueFilteringParameters> singleValueFilteringParameters = new ArrayList<>();
ArrayNode arrayNode = (ArrayNode) jsonNode;
arrayNode.forEach(node -> {
String field = node.get("field").asText();
String operator = node.get("operator").asText();
if (!StringUtils.isEmpty(field) && !StringUtils.isEmpty(operator)) {
String values = node.get("values").asText();
String[] valuesArray = !StringUtils.isEmpty(values) ? values.split(",") : null;
if (valuesArray != null) {
if (valuesArray.length > 1) {
filteringParametersList.add(ApiFilteringParameters
.of(field, Operator.valueOf(operator), valuesArray));
} else {
singleValueFilteringParameters.add(ApiSingleValueFilteringParameters
.of(field, Operator.valueOf(operator), valuesArray[0]));
}
}
}
});
if (!filteringParametersList.isEmpty()) {
builder.filterings(filteringParametersList);
}
if (!singleValueFilteringParameters.isEmpty()) {
builder.filterings(singleValueFilteringParameters);
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
String start = jobParameters.getString("time_ranges.start");
String end = jobParameters.getString("time_ranges.end");
String since = jobParameters.getString("time_range.since");
String until = jobParameters.getString("time_range.until");
if (!StringUtils.isEmpty(start) && !StringUtils.isEmpty(end)) {
builder.timeRanges(ApiParameters.timeRanges(start, end));
} else if (!StringUtils.isEmpty(since) && !StringUtils.isEmpty(until)) {
builder.timeRange(new TimeRange(since, until));
}
String actionBreakdowns = jobParameters.getString("action_breakdowns");
if (!StringUtils.isEmpty(actionBreakdowns)) {
multiValueMap.set("action_breakdowns", actionBreakdowns);
}
String attributionWindows = jobParameters.getString("action_attribution_windows");
if (attributionWindows != null) {
try {
multiValueMap
.set("action_attribution_windows",
objectMapper.writeValueAsString(attributionWindows.split(",")));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
builder.multiValueMap(multiValueMap);
String pageSize = jobParameters.getString("pageSize");
if (!StringUtils.isEmpty(pageSize)) {
builder.limit(pageSize);
}
return builder.build();
}
};
}
}
Here is how message flows:
1. channel[async_ads_insights] ->IntegrationFlow[amqpOutboundAsyncAdsInsights]->[AMQP]->IntegrationFlow[amqpAdsInsightsAsyncJobRequestFlow]->channel[ad_report_run_polling_channel]->IntegrationFlow[adReportRunPollingLoopFlow]-IF END LOOP->channel[batch_launch_channel] ELSE -> channel[ad_report_run_polling_channel]
2. channel[batch_launch_channel] -> IntegrationFlow[amqpOutbound]-> IntegrationFlow[amqpLaunchSpringBatchJobFlow]
3. Spring Batch Job is launched.
The exception isn't thrown immediately after both instances are started, but after a while. Launching Spring Batch Jobs do succeeds but then start to fail with "A job instance already exists and is complete for..."
The job is for retrieving facebook ads results.
I would appreciate your insights into what is causing the error above.
I also have this configuration which does not use AMQP and works without any problem, but it is only for one instance.
#Configuration
#Conditional(SimpleBatchLaunchCondition.class)
#Slf4j
public class SimpleBatchLaunchIntegrationFlows {
#Autowired
SpringBatchLauncher batchLauncher;
#Autowired
DataSource dataSource;
#Bean(name = "batch_launch_channel")
public MessageChannel batchLaunchChannel() {
return MessageChannels.queue(jdbcChannelMessageStore(), "batch_launch_channel").get();
}
#Bean
public ChannelMessageStoreQueryProvider channelMessageStoreQueryProvider() {
return new MySqlChannelMessageStoreQueryProvider();
}
#Bean
public JdbcChannelMessageStore jdbcChannelMessageStore() {
JdbcChannelMessageStore channelMessageStore = new JdbcChannelMessageStore(dataSource);
channelMessageStore.setChannelMessageStoreQueryProvider(channelMessageStoreQueryProvider());
channelMessageStore.setUsingIdCache(true);
channelMessageStore.setPriorityEnabled(true);
return channelMessageStore;
}
#Bean
public IntegrationFlow launchSpringBatchJobFlow(#Qualifier("batch_launch_channel")
MessageChannel batchLaunchChannel) {
return IntegrationFlows.from(batchLaunchChannel)
.handle(message -> {
String jobName = (String) message.getHeaders().get("job_name");
JobParameters jobParameters = (JobParameters) message.getPayload();
batchLauncher.launchJob(jobName, jobParameters);
}, e->e.poller(Pollers.fixedRate(500).receiveTimeout(500))).get();
}
}
Refer to the Spring Batch documentation. When launching a new instance of a job, the job parameters must be unique.
A common solution is to add a dummy parameter with a UUID or similar but batch provides a strategy, e.g to increment a numeric parameter each time.
EDIT
There is a certain class of exceptions where the members of which are considered irrecoverable (fatal) and it makes no sense to attempt redelivery.
Examples include MessageConversionException - if we can't convert it the first time, we probably can't convert on a redelivery. The ConditionalRejectingErrorHandler is the mechanism by which we detect such exceptions, and cause them to be permanently rejected (and not redelivered).
Other exceptions cause the message to be redelivered by default - there is another property defaultRequeuRejected which can be set to false to permanently reject all failures (not recommended).
You can customize the error handler by subclassing its DefaultExceptionStrategy - override isUserCauseFatal(Throwable cause) to scan the cause tree to look for a JobInstanceAlreadyCompleteException and return true (cause.getCause().getCause() instanceof ...)
I think it was triggered by the error thrown by the "SpringBatch job running already" exception.
That still indicates you have somehow received a second message with the same parameters; it's a different error because the original job is still running; that message is rejected (and requeued) but on subsequent deliveries you get the already completed exception.
So, I still say the root cause of your problem is duplicate requests, but you can avoid the behavior with a customized error handler in the channel adapter's listener container.
I suggest you log the duplicate message so you can figure out why you are getting them.

NServiceBus fails to process message: The requested service 'NServiceBus.Impersonation.ExtractIncomingPrincipal' has not been registered

I am receiving an error when using NServiceBus 4.0.3 with NHibernate 3.3.1 when it's trying to process a message
INFO NServiceBus.Unicast.Transport.TransportReceiver [(null)] <(null)> - Failed to process message
Autofac.Core.Registration.ComponentNotRegisteredException: The requested service 'NServiceBus.Impersonation.ExtractIncomingPrincipal' has not been registered. To avoid this exception, either register a component to provide the service, check for service registration using IsRegistered(), or use the ResolveOptional() method to resolve an optional dependency.
at NServiceBus.Unicast.Transport.TransportReceiver.ProcessMessage(TransportMessage message) in c:\BuildAgent\work\d4de8921a0aabf04\src\NServiceBus.Core\Unicast\Transport\TransportReceiver.cs:line 353
at NServiceBus.Unicast.Transport.TransportReceiver.TryProcess(TransportMessage message) in c:\BuildAgent\work\d4de8921a0aabf04\src\NServiceBus.Core\Unicast\Transport\TransportReceiver.cs:line 233
at NServiceBus.Transports.Msmq.MsmqDequeueStrategy.ProcessMessage(TransportMessage message) in c:\BuildAgent\work\d4de8921a0aabf04\src\NServiceBus.Core\Transports\Msmq\MsmqDequeueStrategy.cs:line 262
at NServiceBus.Transports.Msmq.MsmqDequeueStrategy.Action() in c:\BuildAgent\work\d4de8921a0aabf04\src\NServiceBus.Core\Transports\Msmq\MsmqDequeueStrategy.cs:line 197
2013-08-30 09:35:02,508 [9] WARN NServiceBus.Faults.Forwarder.FaultManager [(null)] <(null)> - Message has failed FLR and will be handed over to SLR for retry attempt: 1, MessageID=8aaed043-b744-49c2-965d-a22a009deb32.
I think it's fairly obvious what that I need to implement or register an "ExtractIncomingPrincipal", but I can't seem to find any documentation on how or whether there is a default one that I can use. I wouldn't have figured that I would have had to register any of the NServiceBus-related services as many of them are already being registered in my IoC implementation.
As requested, here is the EndpointConfig and supporting code I have currently:
[EndpointSLA("00:00:30")]
public class EndpointConfig : IConfigureThisEndpoint, AsA_Server, IWantCustomInitialization {
public void Init() {
Configure.With().ObjectBuilderAdapter().UseInMemoryTimeoutPersister().UseInMemoryGatewayPersister().InMemorySagaPersister().InMemorySubscriptionStorage();
}
}
//public class PrincipalExtractor : ExtractIncomingPrincipal {
// public IPrincipal GetPrincipal(TransportMessage message) {
// return Thread.CurrentPrincipal;
// }
//}
public class ObjectBuilderAdapter : IContainer {
readonly IDependencyInjector injector;
public ObjectBuilderAdapter(IDependencyInjectionBuilder dependencyInjectionBuilder) {
injector = dependencyInjectionBuilder.Create(); //This method does all the common service registrations that I am trying to re-use
//injector.RegisterType<ExtractIncomingPrincipal, PrincipalExtractor>();
}
public void Dispose() {
injector.Dispose();
}
public object Build(Type typeToBuild) {
return injector.Resolve(typeToBuild);
}
public IContainer BuildChildContainer() {
return new ObjectBuilderAdapter(new DependencyInjectorBuilder());
}
public IEnumerable<object> BuildAll(Type typeToBuild) {
return injector.ResolveAll(typeToBuild);
}
public void Configure(Type component, DependencyLifecycle dependencyLifecycle) {
injector.RegisterType(component);
}
public void Configure<T>(Func<T> component, DependencyLifecycle dependencyLifecycle) {
injector.RegisterType(component);
}
public void ConfigureProperty(Type component, string property, object value) {
if (injector is AutofacDependencyInjector) {
((AutofacDependencyInjector)injector).ConfigureProperty(component, property, value);
} else {
Debug.WriteLine("Configuring {0} for property {1} but we don't handle this scenario.", component.Name, property);
}
}
public void RegisterSingleton(Type lookupType, object instance) {
injector.RegisterInstance(lookupType, instance);
}
public bool HasComponent(Type componentType) {
return injector.IsRegistered(componentType);
}
public void Release(object instance) { }
}
public static class Extensions {
public static Configure ObjectBuilderAdapter(this Configure config) {
ConfigureCommon.With(config, new ObjectBuilderAdapter(new DependencyInjectorBuilder()));
return config;
}
}
I removed the IWantCustomInitialization (left over from something else I had tried earlier) interface implementation on the class and my service now processes the message. There are errors still (relating to trying to connect to Raven [even though I thought I am using everything in-memory), but it's processing the message.

Resources