Spring Integration call another handler method after aggregation - spring

I am developing a system which will read and process file from a directory. Once all the file has been processed it will call a method which in turn generates a file. Also, it should route/process the files based on file name, I have used spring integration router for the same. Below is the code snippet of the Integration. My question is, this is not working if I remove any of the line .channel(aggregatorOutputChannel()) or .channel(confirmChannel()), also I have to keep the same channel .channel(aggregatorOutputChannel()) before and after the aggregator. Why do I need all 3 channel declaration? if this is wrong how to correct it.
I am using JDK 8, Spring 5, Spring boot 2.0.4.
#Configuration
#EnableIntegration
public class IntegrationConfig {
#Value("${agent.demographic.input.directory}")
private String inputDir;
#Value("${agent.demographic.output.directory}")
private String outputDir;
#Value("${confirmationfile.directory}")
private String confirmDir;
#Value("${input.scan.frequency: 2}")
private long scanFrequency;
#Value("${processing.waittime: 6000}")
private long messageGroupWaiting;
#Value("${thread.corepoolsize: 10}")
private int corepoolsize;
#Value("${thread.maxpoolsize: 20}")
private int maxpoolsize;
#Value("${thread.queuecapacity: 1000}")
private int queuedepth;
#Bean
public MessageSource<File> inputFileSource() {
FileReadingMessageSource src = new FileReadingMessageSource();
src.setDirectory(new File(inputDir));
src.setAutoCreateDirectory(true);
ChainFileListFilter<File> chainFileListFilter = new ChainFileListFilter<>();
chainFileListFilter.addFilter(new AcceptOnceFileListFilter<>() );
chainFileListFilter.addFilter(new RegexPatternFileListFilter("(?i)^.+\\.xml$"));
src.setFilter(chainFileListFilter);
return src;
}
#Bean
public UnZipTransformer unZipTransformer() {
UnZipTransformer unZipTransformer = new UnZipTransformer();
unZipTransformer.setExpectSingleResult(false);
unZipTransformer.setZipResultType(ZipResultType.FILE);
unZipTransformer.setDeleteFiles(true);
return unZipTransformer;
}
#Bean("agentdemographicsplitter")
public UnZipResultSplitter splitter() {
UnZipResultSplitter splitter = new UnZipResultSplitter();
return splitter;
}
#Bean
public DirectChannel outputChannel() {
return new DirectChannel();
}
#Bean
public DirectChannel aggregatorOutputChannel() {
return new DirectChannel();
}
#Bean("confirmChannel")
public DirectChannel confirmChannel() {
return new DirectChannel();
}
#Bean
public MessageHandler fileOutboundChannelAdapter() {
FileWritingMessageHandler adapter = new FileWritingMessageHandler(new File(outputDir));
adapter.setDeleteSourceFiles(true);
adapter.setAutoCreateDirectory(true);
adapter.setExpectReply(true);
adapter.setLoggingEnabled(true);
return adapter;
}
#Bean
public MessageHandler confirmationfileOutboundChannelAdapter() {
FileWritingMessageHandler adapter = new FileWritingMessageHandler(new File(confirmDir));
adapter.setDeleteSourceFiles(true);
adapter.setAutoCreateDirectory(true);
adapter.setExpectReply(false);
adapter.setFileNameGenerator(defaultFileNameGenerator() );
return adapter;
}
#Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corepoolsize);
executor.setMaxPoolSize(maxpoolsize);
executor.setQueueCapacity(queuedepth);
return executor;
}
#Bean
public DefaultFileNameGenerator defaultFileNameGenerator() {
DefaultFileNameGenerator defaultFileNameGenerator = new DefaultFileNameGenerator();
defaultFileNameGenerator.setExpression("payload.name");
return defaultFileNameGenerator;
}
#Bean
public IntegrationFlow confirmGeneration() {
return IntegrationFlows.
from("confirmChannel")
.handle(confirmationfileOutboundChannelAdapter())
.get();
}
#Bean
public IntegrationFlow individualProcessor() {
return flow -> flow.handle("thirdpartyIndividualAgentProcessor","processfile").channel(outputChannel()).handle(fileOutboundChannelAdapter());
}
#Bean
public IntegrationFlow firmProcessor() {
return flow -> flow.handle("thirdpartyFirmAgentProcessor","processfile").channel(outputChannel()).handle(fileOutboundChannelAdapter());
}
#Bean
public IntegrationFlow thirdpartyAgentDemographicFlow() {
return IntegrationFlows
.from(inputFileSource(), spec -> spec.poller(Pollers.fixedDelay(scanFrequency,TimeUnit.SECONDS)))
.channel(MessageChannels.executor(taskExecutor()))
.<File, Boolean>route(f -> f.getName().contains("individual"), m -> m
.subFlowMapping(true, sf -> sf.gateway(individualProcessor()))
.subFlowMapping(false, sf -> sf.gateway(firmProcessor()))
)
.channel(aggregatorOutputChannel())
.aggregate(aggregator -> aggregator.groupTimeout(messageGroupWaiting).correlationStrategy(new CorrelationStrategy() {
#Override
public Object getCorrelationKey(Message<?> message) {
return "xyz";
}
}))
.channel(aggregatorOutputChannel())
.handle("agentDemograpicOutput","generateAgentDemographicFile")
.channel(confirmChannel())
.get();
}
}
Below is the log
2018-09-07 17:29:20.003 DEBUG 10060 --- [ taskExecutor-2] o.s.integration.channel.DirectChannel : preSend on channel 'outputChannel', message: GenericMessage [payload=C:\thirdpartyintg\input\18237232_firm.xml, headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#1a867ae7, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#1a867ae7, file_name=18237232_firm.xml, file_originalFile=C:\thirdpartyintg\input\18237232_firm.xml, id=dd70999a-8b8d-93d2-1a43-a961ac2c339f, file_relativePath=18237232_firm.xml, timestamp=1536366560003}]
2018-09-07 17:29:20.003 DEBUG 10060 --- [ taskExecutor-2] o.s.i.file.FileWritingMessageHandler : fileOutboundChannelAdapter received message: GenericMessage [payload=C:\thirdpartyintg\input\18237232_firm.xml, headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#1a867ae7, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#1a867ae7, file_name=18237232_firm.xml, file_originalFile=C:\thirdpartyintg\input\18237232_firm.xml, id=dd70999a-8b8d-93d2-1a43-a961ac2c339f, file_relativePath=18237232_firm.xml, timestamp=1536366560003}]
2018-09-07 17:29:20.006 DEBUG 10060 --- [ taskExecutor-2] o.s.integration.channel.DirectChannel : postSend (sent=true) on channel 'outputChannel', message: GenericMessage [payload=C:\thirdpartyintg\input\18237232_firm.xml, headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#1a867ae7, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#1a867ae7, file_name=18237232_firm.xml, file_originalFile=C:\thirdpartyintg\input\18237232_firm.xml, id=dd70999a-8b8d-93d2-1a43-a961ac2c339f, file_relativePath=18237232_firm.xml, timestamp=1536366560003}]
2018-09-07 17:29:20.006 DEBUG 10060 --- [ taskExecutor-2] o.s.integration.channel.DirectChannel : postSend (sent=true) on channel 'firmProcessor.input', message: GenericMessage [payload=C:\thirdpartyintg\input\18237232_firm.xml, headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#1a867ae7, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#1a867ae7, file_name=18237232_firm.xml, file_originalFile=C:\thirdpartyintg\input\18237232_firm.xml, id=0e6dcb75-db99-1740-7b58-e9b42bfbf603, file_relativePath=18237232_firm.xml, timestamp=1536366559761}]
2018-09-07 17:29:20.007 DEBUG 10060 --- [ taskExecutor-2] o.s.integration.channel.DirectChannel : preSend on channel 'thirdpartyintgAgentDemographicFlow.channel#2', message: GenericMessage [payload=C:\thirdpartyintg\output\18237232_firm.xml, headers={file_originalFile=C:\thirdpartyintg\input\18237232_firm.xml, id=e6e2a30a-60b9-7cdd-84cc-4977d4c21c97, file_name=18237232_firm.xml, file_relativePath=18237232_firm.xml, timestamp=1536366560007}]
2018-09-07 17:29:20.008 DEBUG 10060 --- [ taskExecutor-2] o.s.integration.channel.DirectChannel : postSend (sent=true) on channel 'thirdpartyintgAgentDemographicFlow.channel#2', message: GenericMessage [payload=C:\thirdpartyintg\output\18237232_firm.xml, headers={file_originalFile=C:\thirdpartyintg\input\18237232_firm.xml, id=e6e2a30a-60b9-7cdd-84cc-4977d4c21c97, file_name=18237232_firm.xml, file_relativePath=18237232_firm.xml, timestamp=1536366560007}]
2018-09-07 17:29:20.009 DEBUG 10060 --- [ taskExecutor-2] o.s.integration.channel.DirectChannel : postSend (sent=true) on channel 'thirdpartyintgAgentDemographicFlow.subFlow#1.channel#0', message: GenericMessage [payload=C:\thirdpartyintg\input\18237232_firm.xml, headers={file_originalFile=C:\thirdpartyintg\input\18237232_firm.xml, id=13713de8-91ce-b1fa-f52d-450d3038cf9c, file_name=18237232_firm.xml, file_relativePath=18237232_firm.xml, timestamp=1536366559757}]
2018-09-07 17:29:26.009 INFO 10060 --- [ask-scheduler-9] o.s.i.a.AggregatingMessageHandler : Expiring MessageGroup with correlationKey[processdate]
2018-09-07 17:29:26.011 DEBUG 10060 --- [ask-scheduler-9] o.s.integration.channel.NullChannel : message sent to null channel: GenericMessage [payload=C:\thirdpartyintg\output\17019222_individual.xml, headers={file_originalFile=C:\thirdpartyintg\input\17019222_individual.xml, id=c654076b-696f-25d4-bded-0a43d1a8ca97, file_name=17019222_individual.xml, file_relativePath=17019222_individual.xml, timestamp=1536366559927}]
2018-09-07 17:29:26.011 DEBUG 10060 --- [ask-scheduler-9] o.s.integration.channel.NullChannel : message sent to null channel: GenericMessage [payload=C:\thirdpartyintg\output\18237232_firm.xml, headers={file_originalFile=C:\thirdpartyintg\input\18237232_firm.xml, id=e6e2a30a-60b9-7cdd-84cc-4977d4c21c97, file_name=18237232_firm.xml, file_relativePath=18237232_firm.xml, timestamp=1536366560007}]

First of all the RegexPatternFileListFilter should be first in the ChainFileListFilter. This way you won't overhead a memory in the AcceptOnceFileListFilter for files which you are not interested in.
You need .channel(confirmChannel()) in the end of the thirdpartyAgentDemographicFlow because this one is an input to your confirmGeneration flow.
I don't think that you .channel(aggregatorOutputChannel()) at all it has to implicit.
You also don't need that .channel(outputChannel()) in the sub-flows.
this is not working
Please, elaborate more: what error you get, how then it works etc...
You also can share some DEBUG logs for the org.springframework.integration to determine how your messages travel.
Also it would help a lot if your share some simple Spring Boot project on GitHub to let us to play with and reproduce according your provided instructions.
UPDATE
Also I've noticed that your aggregator is based on the groupTimeout(). To make it to send aggregated message to downstream you also need to configure there this:
/**
* #param sendPartialResultOnExpiry the sendPartialResultOnExpiry.
* #return the handler spec.
* #see AbstractCorrelatingMessageHandler#setSendPartialResultOnExpiry(boolean)
*/
public S sendPartialResultOnExpiry(boolean sendPartialResultOnExpiry) {
It is false by default, so your messages indeed are sent to the NullChannel.
See more info in the Docs: https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-routing-chapter.html#agg-and-group-to

Related

ExecutionException:Due to: org.springframework.kafka.requestreply.KafkaReplyTimeoutException: Reply timed out using ReplyingKafkaTemplate

I am using kafka to publish both async and sync messages to the broker .One listener would listen to the topic and respond for both sync and async calls. I am using same request topic for both the templates ..
When using fire and forget(Async) I don't see any issues since listener would listen to the messages randomly from topic.When using synchronous call I am getting timeout exception.
Do I need to maintain multiple listeners for different templates ?
With same topic for both synchronous and async operations would there be any issues?
KafkaConfig.java
//Template for synchornous call
#Bean
public ReplyingKafkaTemplate<String, Model, Model> replyingKafkaTemplate (
ProducerFactory<String, Model> pf,
ConcurrentMessageListenerContainer<String, Model> repliesContainer)
{
ReplyingKafkaTemplate<String, Model, Model> replyTemplate =
new ReplyingKafkaTemplate<>(pf, repliesContainer);
replyTemplate.setSharedReplyTopic(true);
return replyTemplate;
}
#Bean //register ConcurrentMessageListenerContainer bean
public ConcurrentMessageListenerContainer<String, Model> repliesContainer (
ConcurrentKafkaListenerContainerFactory<String, Model> containerFactory)
{
ConcurrentMessageListenerContainer<String, Model> repliesContainer =
containerFactory.createContainer("responseTopic");
repliesContainer.getContainerProperties().setGroupId(UUID.randomUUID().toString());
repliesContainer.setAutoStartup(false);
return repliesContainer;
}
//Template for asynchronous call
#Bean
#Qualifier("kafkaTemplate")
public KafkaTemplate<String, Model> kafkaTemplate (
ProducerFactory<String, Model> pf,
ConcurrentKafkaListenerContainerFactory<String, Model> factory)
{
KafkaTemplate<String, Model> kafkaTemplate = new KafkaTemplate<>(pf);
factory.setReplyTemplate(kafkaTemplate);
return kafkaTemplate;
}
Here is service class
#Service
public class KafkaService
{
#Autowired
private ReplyingKafkaTemplate<String, Model, Model> replyingKafkaTemplate;
#Autowired
private KafkaTemplate<String, Model> kafkaTemplate;
#Autowired
private KafkaConfig config;
public Object sendAndReceive (Model model)
{
ProducerRecord<String, Model> producerRecord =
new ProducerRecord("requestTopic", model);
producerRecord.headers()
.add(
new RecordHeader(KafkaHeaders.REPLY_TOPIC, "replyTopic"));
RequestReplyFuture<String, Model, Model> replyFuture =
replyingKafkaTemplate.sendAndReceive(producerRecord, Duration.ofSeconds(timeout));
ConsumerRecord<String, Model> consumerRecord =
replyFuture.get(timeout, TimeUnit.SECONDS);
return consumerRecord.value();
}
public ResponseEntity<Object> send (final Model model)
{
final ProducerRecord<String, Model> producerRecord =
new ProducerRecord("requestTopic", model);
final ListenableFuture<SendResult<String, Model>> future =
kafkaTemplate.send(producerRecord);
final SendResult<String, Model> sendResult = future.get(timeout, TimeUnit.SECONDS);
return new ResponseEntity<>(sendResult, HttpStatus.ACCEPTED);
}
}
Here is the listener class.
#Slf4j
#Service
public class MessageListener
{
#KafkaListener(groupId = "${group.id}", topics = "requestTopic", errorHandler = "customKafkaListenerErrorHandler",containerFactory = "customKafkaListenerContainerFactory")
#SendTo
public Model consumer (Model model)
{
switch (model.getType()) {
case "async":
System.out.println("Async messages are retrieved");
case "sync":
System.out.println("Sync messages are retrieved");
return model;
}
return model;
}
#Bean
public ConcurrentKafkaListenerContainerFactory<?, ?> customKafkaListenerContainerFactory(
ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
ConsumerFactory<Object, Object> kafkaConsumerFactory)
{
ConcurrentKafkaListenerContainerFactory<Object, Object>
concurrentKafkaListenerContainerFactory =
new ConcurrentKafkaListenerContainerFactory<>();
concurrentKafkaListenerContainerFactory.
setConsumerFactory(kafkaConsumerFactory);
concurrentKafkaListenerContainerFactory.getContainerProperties()
.setAckMode(ContainerProperties.AckMode.RECORD);
concurrentKafkaListenerContainerFactory.
setCommonErrorHandler(errorHandler());
configurer.configure(concurrentKafkaListenerContainerFactory, kafkaConsumerFactory);
concurrentKafkaListenerContainerFactory.setReplyTemplate(kafkaTemplate);
return concurrentKafkaListenerContainerFactory;
}
}
application.properties
spring.kafka.consumer.enable-auto-commit=false
spring.kafka.consumer.auto-offset-reset=earliest
Debug Logs:
2022-09-15 15:48:07.771 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : Processing [GenericMessage [payload=com.sample.Model#37a32ae0, headers={kafka_offset=239, kafka_consumer=org.apache.kafka.clients.consumer.KafkaConsumer#59ff0b21, kafka_timestampType=CREATE_TIME, kafka_receivedPartitionId=0, kafka_receivedTopic=requestTopic, kafka_receivedTimestamp=1663282080306, kafka_groupId=consumer_group_new22}]]
2022-09-15 15:48:07.774 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : Listener method returned result [com.sample.Model#37a32ae0] - generating response message for it
2022-09-15 15:48:07.780 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : No replyTopic to handle the reply: com.sample.Model#37a32ae0
2022-09-15 15:50:54.760 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : Processing [GenericMessage [payload=com.sample.Model#3f766126, headers={kafka_offset=240, kafka_consumer=org.apache.kafka.clients.consumer.KafkaConsumer#59ff0b21, kafka_timestampType=CREATE_TIME, kafka_receivedPartitionId=0, kafka_receivedTopic=requestTopic, kafka_receivedTimestamp=1663282254296, kafka_groupId=consumer_group_new22}]]
2022-09-15 15:50:54.760 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : Listener method returned result [com.sample.Model#3f766126] - generating response message for it
2022-09-15 15:50:54.761 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : No replyTopic to handle the reply: com.sample.Model#3f766126
2022-09-15 15:51:44.482 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : Processing [GenericMessage [payload=com.sample.Model#56c68983, headers={kafka_offset=241, kafka_consumer=org.apache.kafka.clients.consumer.KafkaConsumer#59ff0b21, kafka_timestampType=CREATE_TIME, kafka_receivedPartitionId=0, kafka_receivedTopic=requestTopic, kafka_receivedTimestamp=1663282304204, kafka_groupId=consumer_group_new22}]]
2022-09-15 15:51:44.483 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : Listener method returned result [com.sample.Model#56c68983] - generating response message for it
2022-09-15 15:51:44.483 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : No replyTopic to handle the reply: com.sample.Model#56c68983
2022-09-15 15:52:03.237 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : Processing [GenericMessage [payload=com.sample.Model#6682bf3c, headers={kafka_offset=242, kafka_consumer=org.apache.kafka.clients.consumer.KafkaConsumer#59ff0b21, kafka_correlationId=[B#65f4dd3b, kafka_timestampType=CREATE_TIME, kafka_replyTopic=[B#79cca97, kafka_receivedPartitionId=0, kafka_receivedTopic=requestTopic, kafka_receivedTimestamp=1663282322947, kafka_groupId=consumer_group_new22}]]
2022-09-15 15:52:03.237 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : Listener method returned result [com.sample.Model#6682bf3c] - generating response message for it
2022-09-15 15:52:42.585 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : Processing [GenericMessage [payload=com.sample.Model#78a4382d, headers={kafka_offset=243, kafka_consumer=org.apache.kafka.clients.consumer.KafkaConsumer#59ff0b21, kafka_timestampType=CREATE_TIME, kafka_receivedPartitionId=0, kafka_receivedTopic=requestTopic, kafka_receivedTimestamp=1663282362320, kafka_groupId=consumer_group_new22}]]
2022-09-15 15:52:42.585 DEBUG 35380 --- [ntainer#0-0-C-1] .a.RecordMessagingMessageListenerAdapter : Listener method returned result [com.sample.Model#78a4382d] - generating response message for it
This works exactly as I expected...
#SpringBootApplication
public class So73657031Application {
public static void main(String[] args) {
SpringApplication.run(So73657031Application.class, args);
}
#Bean
ReplyingKafkaTemplate<String, String, String> rkt(ProducerFactory<String, String> pf,
ConcurrentKafkaListenerContainerFactory<String, String> factory,
KafkaTemplate<String, String> template) {
factory.setReplyTemplate(template);
ConcurrentMessageListenerContainer<String, String> container = factory.createContainer("so73657031-replies");
container.getContainerProperties().setGroupId("so73657031-replies");
return new ReplyingKafkaTemplate<>(pf, container);
}
#Bean
KafkaTemplate<String, String> template(ProducerFactory<String, String> pf) {
return new KafkaTemplate<>(pf);
}
#Bean
NewTopic topic1() {
return TopicBuilder.name("so73657031").partitions(1).replicas(1).build();
}
#Bean
NewTopic topic2() {
return TopicBuilder.name("so73657031-replies").partitions(1).replicas(1).build();
}
#Bean
public ApplicationRunner runner(ReplyingKafkaTemplate<String, String, String> rTemplate,
KafkaTemplate<String, String> template) {
return args -> {
RequestReplyFuture<String, String, String> future =
rTemplate.sendAndReceive(new ProducerRecord<String, String>("so73657031", 0, null, "test"),
Duration.ofSeconds(30));
System.out.println(future.getSendFuture().get(10, TimeUnit.SECONDS).getRecordMetadata());
System.out.println(future.get(30, TimeUnit.SECONDS).value());
ListenableFuture<SendResult<String, String>> future2 = template.send("so73657031", "oneWay");
System.out.println(future2.get(10, TimeUnit.SECONDS).getRecordMetadata());
};
}
}
#Component
class Listener {
#KafkaListener(id = "so73657031", topics = "so73657031")
#SendTo
String listen(String in) {
System.out.println(in);
return in.toUpperCase();
}
}
logging.level.root=warn
logging.level.org.springframework.kafka.listener.adapter=debug
so73657031-0#2
2022-09-15 15:36:34.496 DEBUG 71184 --- [o73657031-0-C-1] .a.RecordMessagingMessageListenerAdapter : Processing [GenericMessage [payload=test, headers={kafka_offset=2, kafka_consumer=org.apache.kafka.clients.consumer.KafkaConsumer#1582e8e4, kafka_correlationId=[B#2a266829, kafka_timestampType=CREATE_TIME, kafka_deliveryAttempt=1, kafka_replyTopic=[B#3dad3e81, kafka_receivedPartitionId=0, kafka_receivedTopic=so73657031, kafka_receivedTimestamp=1663270594381, kafka_groupId=so73657031}]]
test
2022-09-15 15:36:34.499 DEBUG 71184 --- [o73657031-0-C-1] .a.RecordMessagingMessageListenerAdapter : Listener method returned result [TEST] - generating response message for it
TEST
so73657031-0#3
2022-09-15 15:36:34.519 DEBUG 71184 --- [o73657031-0-C-1] .a.RecordMessagingMessageListenerAdapter : Processing [GenericMessage [payload=oneWay, headers={kafka_offset=3, kafka_consumer=org.apache.kafka.clients.consumer.KafkaConsumer#1582e8e4, kafka_timestampType=CREATE_TIME, kafka_deliveryAttempt=1, kafka_receivedPartitionId=0, kafka_receivedTopic=so73657031, kafka_receivedTimestamp=1663270594514, kafka_groupId=so73657031}]]
oneWay
2022-09-15 15:36:34.519 DEBUG 71184 --- [o73657031-0-C-1] .a.RecordMessagingMessageListenerAdapter : Listener method returned result [ONEWAY] - generating response message for it
2022-09-15 15:36:34.519 DEBUG 71184 --- [o73657031-0-C-1] .a.RecordMessagingMessageListenerAdapter : No replyTopic to handle the reply: ONEWAY

route FROM and route TO with spring cloud stream and functions

I have some issues with the new routing feature in spring cloud stream
I tried to implement a simple scenario, I want to send a message with a header spring.cloud.function.definition = consume1 or consume2
I expect that consume1 or consume2 should be called based on what is sent on the header but the methods are called randomly.
I send the message to the exchange consumer using the rabbit admin console
I'm having the following logs:
2020-02-27 14:48:25.896 INFO 22132 --- [ consumer.app-1] com.example.demo.TestConsumer : ==============>consume1 messge [[payload=ok, headers={amqp_receivedDeliveryMode=NON_PERSISTENT, amqp_receivedRoutingKey=#, amqp_receivedExchange=consumer, amqp_deliveryTag=1, deliveryAttempt=1, amqp_consumerQueue=consumer.app, amqp_redelivered=false, id=9a4dff25-88ef-4d76-93e2-c8719cda122d, spring.cloud.function.definition=consume1, amqp_consumerTag=amq.ctag-gGChFNCKIVd25yyR9H6-fQ, sourceData=(Body:'[B#3a92faa7(byte[2])' MessageProperties [headers={spring.cloud.function.definition=consume1}, contentLength=0, receivedDeliveryMode=NON_PERSISTENT, redelivered=false, receivedExchange=consumer, receivedRoutingKey=#, deliveryTag=1, consumerTag=amq.ctag-gGChFNCKIVd25yyR9H6-fQ, consumerQueue=consumer.app]), timestamp=1582811303347}]]
2020-02-27 14:48:25.984 INFO 22132 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-02-27 14:48:25.984 INFO 22132 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-02-27 14:48:25.991 INFO 22132 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 7 ms
2020-02-27 14:48:26.037 INFO 22132 --- [oundedElastic-1] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageChannel customer-1
2020-02-27 14:48:26.111 INFO 22132 --- [oundedElastic-1] o.s.c.s.m.DirectWithAttributesChannel : Channel 'application.customer-1' has 1 subscriber(s).
2020-02-27 14:48:26.116 INFO 22132 --- [oundedElastic-1] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [localhost:5672]
2020-02-27 14:48:26.123 INFO 22132 --- [oundedElastic-1] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory.publisher#32438e24:0/SimpleConnection#3e58666d [delegate=amqp://guest#127.0.0.1:5672/, localPort= 62514]
2020-02-27 14:48:26.139 INFO 22132 --- [-1.customer-1-1] o.s.i.h.s.MessagingMethodInvokerHelper : Overriding default instance of MessageHandlerMethodFactory with provided one.
2020-02-27 14:48:26.140 INFO 22132 --- [-1.customer-1-1] com.example.demo.TestSink : Data received customer-1...body
2020-02-27 14:49:14.185 INFO 22132 --- [ consumer.app-1] o.s.i.h.s.MessagingMethodInvokerHelper : Overriding default instance of MessageHandlerMethodFactory with provided one.
2020-02-27 14:49:14.194 INFO 22132 --- [ consumer.app-1] com.example.demo.TestConsumer : ==============>consume2 messge [[payload=ok, headers={amqp_receivedDeliveryMode=NON_PERSISTENT, amqp_receivedRoutingKey=#, amqp_receivedExchange=consumer, amqp_deliveryTag=1, deliveryAttempt=1, amqp_consumerQueue=consumer.app, amqp_redelivered=false, id=33581edb-2832-1c92-b765-a05794512b34, spring.cloud.function.definition=consume1, amqp_consumerTag=amq.ctag-RIp2nZdcG2a0hNQeImwtBw, sourceData=(Body:'[B#8159793(byte[2])' MessageProperties [headers={spring.cloud.function.definition=consume1}, contentLength=0, receivedDeliveryMode=NON_PERSISTENT, redelivered=false, receivedExchange=consumer, receivedRoutingKey=#, deliveryTag=1, consumerTag=amq.ctag-RIp2nZdcG2a0hNQeImwtBw, consumerQueue=consumer.app]), timestamp=1582811354186}]]
2020-02-27 14:49:14.203 INFO 22132 --- [oundedElastic-1] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageChannel customer-2
2020-02-27 14:49:14.213 INFO 22132 --- [oundedElastic-1] o.s.c.s.m.DirectWithAttributesChannel : Channel 'application.customer-2' has 1 subscriber(s).
2020-02-27 14:49:14.216 INFO 22132 --- [-2.customer-2-1] o.s.i.h.s.MessagingMethodInvokerHelper : Overriding default instance of MessageHandlerMethodFactory with provided one.
2020-02-27 14:49:14.216 INFO 22132 --- [-2.customer-2-1] com.example.demo.TestSink : Data received customer-2...body
application.yml
spring:
main:
allow-bean-definition-overriding: true
spring.cloud.stream:
function.definition: supplier;receive1;receive2;consume1;consume2
function.routing:
enabled: true
bindings:
consume1-in-0.destination: consumer
consume1-in-0.group: app
consume2-in-0.destination: consumer
consume2-in-0.group: app
receive1-in-0.destination: customer-1
receive1-in-0.group: customer-1
receive2-in-0.destination: customer-2
receive2-in-0.group: customer-2
DemoApplication.java
import com.fasterxml.jackson.databind.ObjectMapper
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpStatus
import org.springframework.messaging.Message
import org.springframework.messaging.support.MessageBuilder
import org.springframework.stereotype.Component
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod.GET
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.RestTemplate
import reactor.core.publisher.EmitterProcessor
import reactor.core.publisher.Flux
import java.util.function.Consumer
import java.util.function.Supplier
#SpringBootApplication
class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
#RestController
class DynamicDestinationController(private val jsonMapper: ObjectMapper) {
private val processor: EmitterProcessor<Message<String>> = EmitterProcessor.create<Message<String>>()
#RequestMapping(path = ["/api/dest/{destName}"], method = [GET], consumes = ["*/*"])
#ResponseStatus(HttpStatus.ACCEPTED)
fun handleRequest(#PathVariable destName:String) {
val message: Message<String> = MessageBuilder.withPayload("body")
.setHeader("spring.cloud.stream.sendto.destination", destName).build()
processor.onNext(message)
}
#Bean
fun supplier(): Supplier<Flux<Message<String>>> {
return Supplier { processor }
}
}
const val destResourceUrl = "http://localhost:8080/api/dest"
#Component
class TestConsumer() {
private val restTemplate: RestTemplate = RestTemplate()
private val logger: Log = LogFactory.getLog(javaClass)
#Bean
fun consume1(): Consumer<Message<String>> = Consumer {
logger.info("==============>consume1 messge [[payload=${it.payload}, headers=${it.headers}]]")
restTemplate.getForEntity("$destResourceUrl/customer-1", String::class.java)
}
#Bean
fun consume2(): Consumer<Message<String>> = Consumer {
logger.info("==============>consume2 messge [[payload=${it.payload}, headers=${it.headers}]]")
restTemplate.getForEntity("$destResourceUrl/customer-2", String::class.java)
}
}
#Component
class TestSink {
private val logger: Log = LogFactory.getLog(javaClass)
#Bean
fun receive1(): Consumer<String> = Consumer {
logger.info("Data received customer-1..." + it);
}
#Bean
fun receive2(): Consumer<String> = Consumer {
logger.info("Data received customer-2..." + it);
}
}
Any idea how to fix the route to consumer?
thanks in advance.
demo-repo
Actually I am a bit confused, so let's do one step at the time. Here is the functioning (modelled after yours) app which uses sendto feature allowing you to send messages to the specific (existing and/or dynamically resolved) destinations.
(in java but you can rework it to Kotlin)
#Controller
public class WebSourceApplication {
public static void main(String[] args) {
SpringApplication.run(WebSourceApplication.class,
"--spring.cloud.function.definition=supplier;consA;consB",
"--spring.cloud.stream.bindings.consA-in-0.destination=consumerA",
"--spring.cloud.stream.bindings.consA-in-0.group=consumerA-grp",
"--spring.cloud.stream.bindings.consB-in-0.destination=consumerB",
"--spring.cloud.stream.bindings.consB-in-0.group=consumerB-grp"
);
}
EmitterProcessor<Message<String>> processor = EmitterProcessor.create();
#RequestMapping(path = "/api/dest/{destName}", consumes = "*/*")
#ResponseStatus(HttpStatus.ACCEPTED)
public void delegateToSupplier(#RequestBody String body, #PathVariable String destName) {
Message<String> message = MessageBuilder.withPayload(body)
.setHeader("spring.cloud.stream.sendto.destination", destName)
.build();
processor.onNext(message);
}
#Bean
public Supplier<Flux<Message<String>>> supplier() {
return () -> processor;
}
#Bean
public Consumer<String> consA() {
return v -> {
System.out.println("Consuming from consA: " + v);
};
}
#Bean
public Consumer<String> consB() {
return v -> {
System.out.println("Consuming from consB: " + v);
};
}
}
And when i curl it i get consistent invocation pr the appropriate consumer:
curl -H "Content-Type: application/json" -X POST -d "Hello Spring Cloud Stream" http://localhost:8080/api/dest/consumerA
log: Consuming from consA: Hello Spring Cloud Stream
. . .
curl -H "Content-Type: application/json" -X POST -d "Hello Spring Cloud Stream" http://localhost:8080/api/dest/consumerB
log: Consuming from consB: Hello Spring Cloud Stream
Notice: There is no enable routing property. That feature is mainly aimed to always call one function functionRouter and have it call other functions on your behalf. It is a feature of spring-cloud-function which means it works outside of spring-cloud-srteam and channels/destinations etc.
Isn't that what you are trying to accomplish? Send message to a different destination based on some oath variable in your HTTP request?
Here is an example of a different microservice which receives on routing function which hen routes to different functions
public class FunctionRoutingApplication {
public static void main(String[] args) {
SpringApplication.run(FunctionRoutingApplication.class,
"--spring.cloud.stream.function.routing.enabled=true"
);
}
#Bean
public Consumer<String> consA() {
return v -> {
System.out.println("Consuming from consA: " + v);
};
}
#Bean
public Consumer<String> consB() {
return v -> {
System.out.println("Consuming from consB: " + v);
};
}
}
And that's pretty much it. Go to your broker and send data to functionRouter-in-0 exchange while providing spring.cloud.function.definition=consA/consB headers and you will see consistent invocations.
Am I still missing something?

Remove file from remote using streaming inbound channel adapter spring boot implementation

I am trying to remove file from remote by implementing streaming inbound but connection is closing before adviceChain implementing.
CODE:
#Bean
public SessionFactory<LsEntry> sftpSessionFactory() {
DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
factory.setHost(sftpHost);
factory.setPort(sftpPort);
factory.setUser(sftpUser);
factory.setPassword(sftpPwd);
factory.setAllowUnknownKeys(true);
return new CachingSessionFactory<LsEntry>(factory);
}
#Bean
#InboundChannelAdapter(channel = "stream", poller = #Poller(cron = "2 * * * * ?"))
public MessageSource<InputStream> sftpMessageSource() {
SftpStreamingMessageSource messageSource = new SftpStreamingMessageSource(template());
messageSource.setRemoteDirectory(remoteDirecotry);
messageSource.setFilter(new AcceptAllFileListFilter<>());
return messageSource;
}
#Bean
public SftpRemoteFileTemplate template() {
return new SftpRemoteFileTemplate(sftpSessionFactory());
}
#Bean
#Transformer(inputChannel = "stream", outputChannel = "data")
public org.springframework.integration.transformer.Transformer transformer() {
return new StreamTransformer("UTF-8");
}
#ServiceActivator(inputChannel = "data" ,adviceChain = "afterChain")
#Bean
public MessageHandler handler() {
return new MessageHandler() {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
String fileName = message.getHeaders().get("file_remoteFile").toString();
if (!StringUtils.isEmpty(message.toString())) {
else{
log.info("No file found in the Remote location");
}
}
};
}
#Bean
public ExpressionEvaluatingRequestHandlerAdvice afterChain() {
ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice();
advice.setOnSuccessExpression(
"#template.remove(headers['file_remoteDirectory'] + headers['file_remoteFile'])");
//advice.setOnSuccessExpressionString("#template.remove(headers['file_remoteFile'])");
advice.setPropagateEvaluationFailures(true);
return advice;
}
wherever i search every one is suggesting to implement ExpressionEvaluatingRequestHandlerAdvice but it is throwing me below error.
2018-03-27 12:32:02.618 INFO 23216 --- [ask-scheduler-1] o.s.b.c.l.support.SimpleJobLauncher : Job: [FlowJob: [name=starsBatchJob]] completed with the following parameters: [{JobID=1522168322277}] and the following status: [COMPLETED]
2018-03-27 12:32:02.618 INFO 23216 --- [ask-scheduler-1] c.f.u.config.ParentBatchConfiguration : Job Status Completed
2018-03-27 12:32:02.618 INFO 23216 --- [ask-scheduler-1] c.f.u.config.ParentBatchConfiguration : Total time tokk for Stars Batch execution: 0 seconds.
2018-03-27 12:32:02.618 INFO 23216 --- [ask-scheduler-1] c.f.u.config.ParentBatchConfiguration : Batch Job lock is released
2018-03-27 12:32:02.633 INFO 23216 --- [ask-scheduler-1] com.jcraft.jsch : Disconnecting from hpchd1e.hpc.ford.com port 22
2018-03-27 12:32:02.633 ERROR 23216 --- [ask-scheduler-1] o.s.integration.handler.LoggingHandler : org.springframework.messaging.MessagingException: Dispatcher failed to deliver Message; nested exception is org.springframework.messaging.MessagingException: Failed to execute on session; nested exception is org.springframework.core.NestedIOException: Failed to remove file: 2: No such file; nested exception is 2
I had this problem. My path to the remote file was incorrect. I needed a trailing /. It is a little difficult to see since the path is being created inside a Spel Expression. You can see the path using the following in the handleMessage() method.
String remoteDirectory = (String) message.getHeaders().get("file_remoteDirectory");
String remoteFile = (String) message.getHeaders().get("file_remoteFile");
I did have to use the advice.setOnSuccessExpressionString("#template.remove(headers['file_remoteFile'])"); that is commented out above instead of advice.setOnSuccessExpression"#template.remove(headers['file_remoteDirectory'] + headers['file_remoteFile'])");
It is incorrect in the documentation https://docs.spring.io/spring-integration/reference/html/sftp.html#sftp-streaming which is why I believe people who struggle with this lose faith in the doc. But this seems to be the only error.

How change default channel on Spring Integration Flow with Java DSL

I do not understand very well, as I can change the error channel for all my integration flow. I need to handle exceptions like InvalidAccessTokenException that can be thrown in a subflow inside the router.
What I've tried is to handle exceptions from the default channel "errorChannel" by:
#Bean
public IntegrationFlow errorFlow() {
return IntegrationFlows.from("errorChannel")
.handle("errorService", "handleError")
.get();
}
This error is treated by a method with the following signature:
void handleError(Message<Exception> exception)
But the default behavior persists, that is to say it still shows the trace by console.
So my question is: in java DSL how can I configure an error channel? Is it possible to map a group of exceptions to a particular error channel to make the management service of that group more cohesive?.
The configuration of my integration flow I explain below:
#Configuration
#IntegrationComponentScan
public class InfrastructureConfiguration {
private static Logger logger = LoggerFactory.getLogger(InfrastructureConfiguration.class);
#Autowired
private IFacebookService facebookService;
#Autowired
private IInstagramService instagramService;
#Autowired
private IYoutubeService youtubeService;
/**
* The Pollers builder factory can be used to configure common bean definitions or
* those created from IntegrationFlowBuilder EIP-methods
*/
#Bean(name = PollerMetadata.DEFAULT_POLLER)
public PollerMetadata poller() {
return Pollers.fixedDelay(10, TimeUnit.SECONDS).get();
}
#Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
return executor;
}
/**
* MongoDbMessageSource is an instance of MessageSource which returns a Message with a payload
* which is the result of execution of a Query
*/
#Bean
#Autowired
public MessageSource<Object> mongoMessageSource(MongoDbFactory mongo) {
MongoDbMessageSource messageSource = new MongoDbMessageSource(mongo, new LiteralExpression("{}"));
messageSource.setExpectSingleResult(false);
messageSource.setEntityClass(UserEntity.class);
messageSource.setCollectionNameExpression(new LiteralExpression("users"));
return messageSource;
}
#Bean
#ServiceActivator(inputChannel = "storeChannel")
public MessageHandler mongodbAdapter(MongoDbFactory mongo) throws Exception {
MongoDbStoringMessageHandler adapter = new MongoDbStoringMessageHandler(mongo);
adapter.setCollectionNameExpression(new LiteralExpression("comments"));
return adapter;
}
#Bean
public IntegrationFlow errorFlow() {
return IntegrationFlows.from("errorChannel")
.handle("errorService", "handleError")
.get();
}
#Bean
#Autowired
public IntegrationFlow processUsers(MongoDbFactory mongo, PollerMetadata poller) {
return IntegrationFlows.from(mongoMessageSource(mongo), c -> c.poller(poller))
.<List<UserEntity>, Map<ObjectId, List<SocialMediaEntity>>>transform(userEntitiesList
-> userEntitiesList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getSocialMedia))
)
.split(new AbstractMessageSplitter() {
#Override
protected Object splitMessage(Message<?> msg) {
return ((Map<ObjectId, List<SocialMediaEntity>>) msg.getPayload()).entrySet();
}
})
.channel("directChannel_1")
.enrichHeaders(s -> s.headerExpressions(h -> h.put("user-id", "payload.key")))
.split(new AbstractMessageSplitter() {
#Override
protected Object splitMessage(Message<?> msg) {
return ((Entry<ObjectId, List<SocialMediaEntity>>) msg.getPayload()).getValue();
}
})
.channel(MessageChannels.executor("executorChannel", this.taskExecutor()))
.<SocialMediaEntity, SocialMediaTypeEnum>route(p -> p.getType(),
m
-> m.subFlowMapping(SocialMediaTypeEnum.FACEBOOK,
sf -> sf.handle(SocialMediaEntity.class, (p, h) -> facebookService.getComments(p.getAccessToken())))
.subFlowMapping(SocialMediaTypeEnum.YOUTUBE,
sf -> sf.handle(SocialMediaEntity.class, (p, h) -> youtubeService.getComments(p.getAccessToken())))
.subFlowMapping(SocialMediaTypeEnum.INSTAGRAM,
sf -> sf.handle(SocialMediaEntity.class, (p, h) -> instagramService.getComments(p.getAccessToken())))
)
.channel("directChannel_2")
.aggregate()
.channel("directChannel_3")
.<List<List<CommentEntity>>, List<CommentEntity>>transform(comments ->
comments.stream().flatMap(List::stream).collect(Collectors.toList()))
.aggregate()
.channel("directChannel_4")
.<List<List<CommentEntity>>, List<CommentEntity>>transform(comments ->
comments.stream().flatMap(List::stream).collect(Collectors.toList()))
.channel("storeChannel")
.get();
}
#PostConstruct
protected void init(){
Assert.notNull(facebookService, "The Facebook Service can not be null");
Assert.notNull(instagramService, "The Instagram Service can not be null");
Assert.notNull(youtubeService, "The Youtube Service can not be null");
}
}
An example of an exception that can be launched in any of the social networking services is this:
public class InvalidAccessTokenException extends RuntimeException {
private SocialMediaTypeEnum socialMediaType;
private String accessToken;
public InvalidAccessTokenException(SocialMediaTypeEnum socialMediaType, String accessToken) {
this.socialMediaType = socialMediaType;
this.accessToken = accessToken;
}
public SocialMediaTypeEnum getSocialMediaType() {
return socialMediaType;
}
public String getAccessToken() {
return accessToken;
}
}
Is it possible to bind this exception to a particular error channel?.
Thanks in advance.
I have tried changing the error channel using the PoolSpec as follows:
#Bean(name = PollerMetadata.DEFAULT_POLLER)
public PollerMetadata poller() {
return Pollers.fixedDelay(10, TimeUnit.SECONDS)
.errorChannel("customErrorChannel")
.get();
}
But messages still continue to go to the default channel 'errorChannel'.
Here is an excerpt of the log messages:
2017-07-25 20:20:51.922 DEBUG 3268 --- [ taskExecutor-4] o.s.i.channel.PublishSubscribeChannel : preSend on channel 'errorChannel', message: ErrorMessage [payload=org.springframework.messaging.MessageHandlingException: nested exception is sanchez.sanchez.sergio.exception.InvalidAccessTokenException, failedMessage=GenericMessage [payload=SocialMediaEntity{id=59778bf93681ac0cc4e20089, accessToken=maite_access_token_facebook, type=FACEBOOK, invalidToken=false}, headers={sequenceNumber=1, sequenceDetails=[[35dd7519-59cd-f0ea-69b2-c5fbb7c1c57f, 2, 5]], mongo_collectionName=users, sequenceSize=3, user-id=59778bf93681ac0cc4e20094, correlationId=6665f8ee-2bc9-e6c0-17de-35d63a0afaaa, id=6925b3ea-0b30-3304-b7f0-d235959d7db1, timestamp=1501006851466}], headers={id=6eb1789c-21d7-1814-48ee-77553abd99b9, timestamp=1501006851922}]
2017-07-25 20:20:51.923 DEBUG 3268 --- [ taskExecutor-5] o.s.integration.handler.LoggingHandler : _org.springframework.integration.errorLogger.handler received message: ErrorMessage [payload=org.springframework.messaging.MessageHandlingException: nested exception is sanchez.sanchez.sergio.exception.InvalidAccessTokenException, failedMessage=GenericMessage [payload=SocialMediaEntity{id=59778bf93681ac0cc4e2008c, accessToken=david_access_token_facebook, type=FACEBOOK, invalidToken=false}, headers={sequenceNumber=1, sequenceDetails=[[35dd7519-59cd-f0ea-69b2-c5fbb7c1c57f, 4, 5]], mongo_collectionName=users, sequenceSize=3, user-id=59778bf93681ac0cc4e20095, correlationId=254f918f-52d2-1cff-7ba5-7343a39a8941, id=3a5989df-2af7-56d0-2697-5fcaf75a2891, timestamp=1501006851527}], headers={id=c270d1ef-68ac-cb6b-245f-11e567b5b7e8, timestamp=1501006851922}]
2017-07-25 20:20:51.922 DEBUG 3268 --- [ taskExecutor-3] o.s.integration.handler.LoggingHandler : _org.springframework.integration.errorLogger.handler received message: ErrorMessage [payload=org.springframework.messaging.MessageHandlingException: nested exception is sanchez.sanchez.sergio.exception.InvalidAccessTokenException, failedMessage=GenericMessage [payload=SocialMediaEntity{id=59778bf93681ac0cc4e2008f, accessToken=elena_access_token_facebook, type=FACEBOOK, invalidToken=false}, headers={sequenceNumber=1, sequenceDetails=[[35dd7519-59cd-f0ea-69b2-c5fbb7c1c57f, 5, 5]], mongo_collectionName=users, sequenceSize=3, user-id=59778bf93681ac0cc4e20096, correlationId=1aba58bf-733e-f1ed-a82e-67f482ff3531, id=5eb9c517-f451-e2d6-938d-e436bb362aef, timestamp=1501006851549}], headers={id=30a965d3-f829-d6cf-c971-7b4ed2f15f88, timestamp=1501006851922}]
2017-07-25 20:20:51.923 DEBUG 3268 --- [ taskExecutor-4] o.s.integration.handler.LoggingHandler : _org.springframework.integration.errorLogger.handler received message: ErrorMessage [payload=org.springframework.messaging.MessageHandlingException: nested exception is sanchez.sanchez.sergio.exception.InvalidAccessTokenException, failedMessage=GenericMessage [payload=SocialMediaEntity{id=59778bf93681ac0cc4e20089, accessToken=maite_access_token_facebook, type=FACEBOOK, invalidToken=false}, headers={sequenceNumber=1, sequenceDetails=[[35dd7519-59cd-f0ea-69b2-c5fbb7c1c57f, 2, 5]], mongo_collectionName=users, sequenceSize=3, user-id=59778bf93681ac0cc4e20094, correlationId=6665f8ee-2bc9-e6c0-17de-35d63a0afaaa, id=6925b3ea-0b30-3304-b7f0-d235959d7db1, timestamp=1501006851466}], headers={id=6eb1789c-21d7-1814-48ee-77553abd99b9, timestamp=1501006851922}]
2017-07-25 20:20:51.927 ERROR 3268 --- [ taskExecutor-3] o.s.integration.handler.LoggingHandler : org.springframework.messaging.MessageHandlingException: nested exception is sanchez.sanchez.sergio.exception.InvalidAccessTokenException, failedMessage=GenericMessage [payload=SocialMediaEntity{id=59778bf93681ac0cc4e2008f, accessToken=elena_access_token_facebook, type=FACEBOOK, invalidToken=false}, headers={sequenceNumber=1, sequenceDetails=[[35dd7519-59cd-f0ea-69b2-c5fbb7c1c57f, 5, 5]], mongo_collectionName=users, sequenceSize=3, user-id=59778bf93681ac0cc4e20096, correlationId=1aba58bf-733e-f1ed-a82e-67f482ff3531, id=5eb9c517-f451-e2d6-938d-e436bb362aef, timestamp=1501006851549}]
at org.springframework.integration.dsl.LambdaMessageProcessor.processMessage(LambdaMessageProcessor.java:130)
at org.springframework.integration.handler.ServiceActivatingHandler.handleRequestMessage(ServiceActivatingHandler.java:89)
I have tried two new approaches to change the default error channel:
Create a custom ErrorHandler with the error channel and declare it in the PoolSpec:
#Bean
public MessageChannel customErrorChannel() {
return MessageChannels.direct("customErrorChannel").get();
}
#Bean
public ErrorHandler errorHandler() {
MessagePublishingErrorHandler messagePublishingErrorHandler = new MessagePublishingErrorHandler();
messagePublishingErrorHandler.setDefaultErrorChannel(customErrorChannel());
return messagePublishingErrorHandler;
}
#Bean(name = PollerMetadata.DEFAULT_POLLER)
public PollerMetadata poller() {
return Pollers.fixedDelay(10, TimeUnit.SECONDS)
.errorHandler(errorHandler())
.get();
}
The error messages are still going to the default channel 'errorChannel'.
Adding the MessageHeaders.ERROR_CHANNEL header to explicitly indicate the error channel:
.enrichHeaders(s ->
s.headerExpressions(h -> h.put("user-id", "payload.key"))
.header(MessageHeaders.ERROR_CHANNEL, "customErrorChannel")
)
If this approach works, error messages are directed to the "customErrorChannel".
Yes, you can do that. There is an ErrorMessageExceptionTypeRouter for similar task. So, you will be able to route your InvalidAccessTokenException to the specific channel.
Also be aware that PollerSpec can be supplied with the errorChannel(), so you don't need to worry that all your exceptions go to the default errorChannel.
UPDATE
OK. After some your code investigation I see this:
.channel(MessageChannels.executor("executorChannel", this.taskExecutor()))
That means you shift your message to different thread and, therefore, any exception there is already far away from the try...catch of the poller's algorithm to send to your custom customErrorChannel.
The ExecutorChannel has this logic:
if (!(this.executor instanceof ErrorHandlingTaskExecutor)) {
ErrorHandler errorHandler = new MessagePublishingErrorHandler(
new BeanFactoryChannelResolver(this.getBeanFactory()));
this.executor = new ErrorHandlingTaskExecutor(this.executor, errorHandler);
}
Where that MessagePublishingErrorHandler is really based on the errorChannel by default. What you can do here is something like declaration of similar bean for the taskExecutor() bean and injection your customErrorChannel into the MessagePublishingErrorHandler.
Another option which should work here with the MessagePublishingErrorHandler is errorChannel header population upstream the executorChannel definition.

DestinationResolutionException: no output-channel or replyChannel header available

I am implementing a flow where using a MongoDbMessageSource I get a list of users and I want to process each document in parallel. For this I use the default behavior of Split.
But the following error occurs after the split:
o.s.i.channel.PublishSubscribeChannel : preSend on channel 'errorChannel', message: ErrorMessage [payload=org.springframework.messaging.MessagingException: Dispatcher failed to deliver Message; nested exception is org.springframework.messaging.core.DestinationResolutionException: no output-channel or replyChannel header available, failedMessage=GenericMessage [payload=UserEntity{id=5974dfe53681ac160c78dc0f, firstName=David, lastName=García, age=14, socialMedia=[]}, headers={sequenceNumber=4, correlationId=8f8f7b7a-832a-8942-1922-26b6a7529091, id=bb373e42-d59c-42e6-d221-68bf1f56fec3, mongo_collectionName=users, sequenceSize=5, timestamp=1500831727759}], headers={id=9187ffcd-8c79-eb1e-8791-1cfe558ab134, timestamp=1500831727762}]
The code is as follows:
#Configuration
#IntegrationComponentScan
public class InfrastructureConfiguration {
private static Logger logger = LoggerFactory.getLogger(InfrastructureConfiguration.class);
/**
* The Pollers builder factory can be used to configure common bean definitions or
* those created from IntegrationFlowBuilder EIP-methods
*/
#Bean(name = PollerMetadata.DEFAULT_POLLER)
public PollerMetadata poller() {
return Pollers.fixedDelay(10, TimeUnit.SECONDS).get();
}
#Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
return executor;
}
/**
*
* MongoDbMessageSource is an instance of MessageSource which returns a Message with a payload
* which is the result of execution of a Query
*/
#Bean
#Autowired
public MessageSource<Object> mongoMessageSource(MongoDbFactory mongo) {
MongoDbMessageSource messageSource = new MongoDbMessageSource(mongo, new LiteralExpression("{}"));
messageSource.setExpectSingleResult(false);
messageSource.setEntityClass(UserEntity.class);
messageSource.setCollectionNameExpression(new LiteralExpression("users"));
return messageSource;
}
#Bean
public DirectChannel inputChannel() {
return new DirectChannel();
}
#Bean
#Autowired
public IntegrationFlow processUsers(MongoDbFactory mongo, PollerMetadata poller) {
return IntegrationFlows.from(mongoMessageSource(mongo), c -> c.poller(poller))
.split()
.channel(MessageChannels.executor("executorChannel", this.taskExecutor()))
.handle((GenericHandler<UserEntity>) (payload, headers) -> {
logger.debug("user:" + payload + " on thread "
+ Thread.currentThread().getName());
return payload;
})
.aggregate()
.get();
}
}
Does anyone know I'm doing wrong? Thanks in advance.
Use MessageHandler as suggested Barath:
#Bean
#Autowired
public IntegrationFlow processUsers(MongoDbFactory mongo, PollerMetadata poller) {
return IntegrationFlows.from(mongoMessageSource(mongo), c -> c.poller(poller))
.split()
.channel(MessageChannels.executor("executorChannel", this.taskExecutor()))
.wireTap(sf -> sf.handle(user -> logger.debug("user:" + user.getPayload().toString() + " on thread " + Thread.currentThread().getName())))
.aggregate()
.get();
}
The error persists, the complete trace of the error I put below:
2017-07-23 21:46:36.785 DEBUG 15148 --- [ taskExecutor-4] o.s.integration.handler.LoggingHandler : _org.springframework.integration.errorLogger.handler received message: ErrorMessage [payload=org.springframework.messaging.MessagingException: Dispatcher failed to deliver Message; nested exception is org.springframework.messaging.core.DestinationResolutionException: no output-channel or replyChannel header available, failedMessage=GenericMessage [payload=UserEntity{id=5974fd123681ac3b2c5c343a, firstName=David, lastName=García, age=14, socialMedia=[]}, headers={sequenceNumber=4, correlationId=da7be297-992b-8f5a-d41c-58a89e654fcc, id=eb55d6bf-8108-4be5-8b32-6260d4ceea9b, mongo_collectionName=users, sequenceSize=5, timestamp=1500839196770}], headers={id=83a833cd-ac87-f6be-fb75-8d6907e3d194, timestamp=1500839196784}]
2017-07-23 21:46:36.788 ERROR 15148 --- [ taskExecutor-4] o.s.integration.handler.LoggingHandler : org.springframework.messaging.MessagingException: Dispatcher failed to deliver Message; nested exception is org.springframework.messaging.core.DestinationResolutionException: no output-channel or replyChannel header available, failedMessage=GenericMessage [payload=UserEntity{id=5974fd123681ac3b2c5c343a, firstName=David, lastName=García, age=14, socialMedia=[]}, headers={sequenceNumber=4, correlationId=da7be297-992b-8f5a-d41c-58a89e654fcc, id=eb55d6bf-8108-4be5-8b32-6260d4ceea9b, mongo_collectionName=users, sequenceSize=5, timestamp=1500839196770}]
at org.springframework.integration.dispatcher.AbstractDispatcher.wrapExceptionIfNecessary(AbstractDispatcher.java:133)
at org.springframework.integration.dispatcher.AbstractDispatcher.tryOptimizedDispatch(AbstractDispatcher.java:120)
at org.springframework.integration.dispatcher.UnicastingDispatcher.doDispatch(UnicastingDispatcher.java:148)
at org.springframework.integration.dispatcher.UnicastingDispatcher.access$000(UnicastingDispatcher.java:53)
at org.springframework.integration.dispatcher.UnicastingDispatcher$3.run(UnicastingDispatcher.java:129)
at org.springframework.integration.util.ErrorHandlingTaskExecutor$1.run(ErrorHandlingTaskExecutor.java:55)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.messaging.core.DestinationResolutionException: no output-channel or replyChannel header available
at org.springframework.integration.handler.AbstractMessageProducingHandler.sendOutput(AbstractMessageProducingHandler.java:353)
at org.springframework.integration.handler.AbstractMessageProducingHandler.produceOutput(AbstractMessageProducingHandler.java:269)
at org.springframework.integration.handler.AbstractMessageProducingHandler.sendOutputs(AbstractMessageProducingHandler.java:186)
at org.springframework.integration.aggregator.AbstractCorrelatingMessageHandler.completeGroup(AbstractCorrelatingMessageHandler.java:671)
at org.springframework.integration.aggregator.AbstractCorrelatingMessageHandler.handleMessageInternal(AbstractCorrelatingMessageHandler.java:418)
at org.springframework.integration.handler.AbstractMessageHandler.handleMessage(AbstractMessageHandler.java:127)
at org.springframework.integration.dispatcher.AbstractDispatcher.tryOptimizedDispatch(AbstractDispatcher.java:116)
... 7 more
2017-07-23 21:46:36.788 DEBUG 15148 --- [ taskExecutor-4] o.s.i.channel.PublishSubscribeChannel : postSend (sent=true) on channel 'errorChannel', message: ErrorMessage [payload=org.springframework.messaging.MessagingException: Dispatcher failed to deliver Message; nested exception is org.springframework.messaging.core.DestinationResolutionException: no output-channel or replyChannel header available, failedMessage=GenericMessage [payload=UserEntity{id=5974fd123681ac3b2c5c343a, firstName=David, lastName=García, age=14, socialMedia=[]}, headers={sequenceNumber=4, correlationId=da7be297-992b-8f5a-d41c-58a89e654fcc, id=eb55d6bf-8108-4be5-8b32-6260d4ceea9b, mongo_collectionName=users, sequenceSize=5, timestamp=1500839196770}], headers={id=83a833cd-ac87-f6be-fb75-8d6907e3d194, timestamp=1500839196784}]
Your problem that Aggregator is request-reply component, but you don't have anything after that in your flow. That's why you have that error. You have to decide what to do with the aggregator result.

Resources