Spring Integration Metadatastore - spring-integration

I have added a bean called metadataStore to my spring boot + spring integration application and expected that the ftp synchronization would have been persisted and intact even after a server restart.
Nevertheless, my early tests suggests otherwise; If I start the server and let it pick-up and process 3 tests files and then restart the server, then these same 3 files will be pick-up and processed again - as if no persistent metadataStore was defined at all.
I wonder if I am missing some configuration details when setting up the datastore...
#Configuration
public class MetadataStoreConfiguration {
#Bean
public PropertiesPersistingMetadataStore metadataStore() {
PropertiesPersistingMetadataStore metadataStore = new PropertiesPersistingMetadataStore();
return metadataStore;
}
}
Also, I see in the spring-integration reference manual a short example on how to setup an idempotent receiver and metadata store. Is this what my implementation is lacking?
If that's it and if I have to set this up like in the example, where would I define my metadataStore.get and metadataStore.put calls? the outbound adapter I am using doesn't provide me with an expression attribute... Here is my naive and incomplete attempt at this:
<int-file:inbound-channel-adapter id="inboundLogFile"
auto-create-directory="true"
directory="${sftp.local.dir}"
channel="fileInboundChannel"
prevent-duplicates="true">
<int:poller fixed-rate="${setup.inbound.poller.rate}"/>
</int-file:inbound-channel-adapter>
<int:filter id="fileFilter" input-channel="fileInboundChannel"
output-channel="idempotentServiceChannel"
discard-channel="fileDiscardChannel"
expression="#metadataStore.get(payload.name) == null"/>
This is the outbound adapter used in the example:
<int:outbound-channel-adapter channel="idempotentServiceChannel" expression="#metadataStore.put(payload.name, '')"/>
In my ftp outbound adapter I can't insert the above expression :(
<int-ftp:outbound-channel-adapter id="sdkOutboundFtp"
channel="idempotentServiceChannel"
session-factory="ftpsCachingSessionFactory"
charset="UTF-8"
auto-create-directory="true"
use-temporary-file-name="false"
remote-file-separator="/"
remote-directory-expression="${egnyte.remote.dir}"
* NO EXPRESSION POSSIBLE HERE *
mode="REPLACE">
</int-ftp:outbound-channel-adapter>

By default, the PropertiesPersistingMetadataStore only persists the state on a normal application shutdown; it's kept in memory until then.
In 4.1.2, we changed it to implement Flushable so users can flush the state at any time.
Consider using an FileSystemPersistentAcceptOnceFileListFilter in the local-filter on the inbound adapter instead of a separate filter element. See the documentation for more information.
Starting with version 4.1.5 this filter has an option flushOnUpdate to flush() the metadata store on every update.
Other metadata stores that use an external server (Redis, Mongo, Gemfire) don't need to be flushed.

Related

How to test message-driven-channel-adapter with MockIntegrationContext

I am trying to test a Spring Integration flow that starts off from a message-driven-channel-adapter configured as:
<int-jms:message-driven-channel-adapter id="myAdapter" ... />
My test goes like:
#SpringJUnitConfig(locations = {"my-app-context.xml"})
#SpringIntegrationTest(noAutoStartup = {"myAdapter"})
public class MyIntegrationFlowTest {
#Autowired
private MockIntegrationContext mockIntegrationContext;
#Test
public void myTest() {
...
MessageSource<String> messageSource = () -> new GenericMessage<>("foo");
mockIntegrationContext.substituteMessageSourceFor("myAdapter", messageSource);
...
}
}
I am however getting the following error:
org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'myAdapter' is expected to be of type 'org.springframework.integration.endpoint.SourcePollingChannelAdapter' but was actually of type 'org.springframework.integration.jms.JmsMessageDrivenEndpoint'
How should one specify an alternate source for the channel adapter for testing using the MockIntegrationContext, or by some other method?
The Message Driver Channel Adapter is really not a Source Polling Channel Adapter. So, the substituteMessageSourceFor() is indeed cannot be used for that type of components, which, essentially is a MessageProducerSupport implementation, not a SourcePollingChannelAdapter for a MessageSource.
The difference exists because not all protocols provides a listener-like hooks to spawn some self-managed task to subscribe to. The good example is JDBC, which is only passive system expecting requests. Therefore a polling channel adapter with a JdbcPollingChannelAdapter (which is a MessageSource) implementation must be used to interact with DB in event-driven manner.
Other systems (like JMS in your case) provides some listener (or consumer) API for what we can spawn a while task (see MessageListenerContainer in spring-jms) and let its MessageProducerSupport to emit messages to the channel.
Therefore you need to distinguish for yourself with what type of component you interact before choosing a testing strategy.
Since there is no extra layer in case of message-driver channel adapter, but rather some specific, self-managed MessageProducerSupport impl, we not only provide a particular mocking API, but even don't require to know anything else, but just standard unit testing feature and a message channel this endpoint is producing in the configuration.
So, the solution for you is something like:
#SpringIntegrationTest(noAutoStartup = {"myAdapter"}) - that's fully correct in your code: we really have to stop the real channel adapter to not pollute our testing environment.
You just need to inject into your test class a MessageChannel that id="myAdapter" is producing to. In your test code you just build a Message and send it into this channel. No need to worry about a MockIntegrationContext at all.

Spring integration 'No poller' exception

After excluding
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</exclusion>
I am getting error:
java.lang.IllegalArgumentException: No poller has been defined for Annotation-based endpoint, and no
default poller is available within the context
I have just one QueueChannel (also have MessageAggregator - might be acting as queue?), and few PublishSubscribeChannel
Anyway without exclusion works fine.
What could be the issue? Why exclusion of dependency spring-cloud-stream causes this?
Because Spring Cloud Stream creates a PollerMetadata bean with the PollerMetadata.DEFAULT_POLLER name for us. for its own purpose and for convenience and according Spring Cloud conventions.
Spring Integration by itself doesn't do any convenience assumptions and doesn't create a default poller for us.
You just need to go into your #Configuration and specify there a PollerMetadata with respective trigger for polling logic. Otherwise you can configure a local poller for that MessageAggregator endpoint which consumes from the mentioned QueueChannel.
See docs for more info: https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints.html#endpoint-pollingconsumer

Poll different remote directories using SftpInboundAdapter and RemoteDirectoryExpression

I have an application that uses spring integration to poll files from the SFTP and process them. The structure of the sftp will be always similar, but new folders are going to be created. Example:
/sales/client1/in
/sales/client2/in
...
/sales/clientN/in
I'm using a Sftp Inbound Adapter to poll the SFTP server and start the flow, something like:
#Bean
public IntegrationFlow flow() {
return IntegrationFlows.from(
Sftp.inboundAdapter(sftpSessionFactory())
.preserveTimestamp(true)
.localDirectory(new File("sftp-inbound"))
.deleteRemoteFiles(true)
.autoCreateLocalDirectory(true)
.remoteDirectoryExpression(
EXPRESSION_PARSER.parseExpression("#directoryProvider.getDirectory()"))
.filter(getFilter())
, e -> e.id("sftpInboundAdapter")
.autoStartup(true)
.poller(Pollers.fixedDelay(5, TimeUnit.SECONDS)))
.log()
.channel("processing")
.get();
}
The bean directoryProvider will get the folders from the DB and will go in round-robin returning each folder, which according to what I understood from the documentation:
Starting with version 4.2, you can specify remote-directory-expression instead of remote-directory, letting you dynamically determine the directory on each poll — for example, remote-directory-expression="#myBean.determineRemoteDir()"
it's what I'm needing, however, the method getDirectory is only executed twice when the IntegrationFlow is created and instead of getting a new folder on each poll, it uses the second one that was retrieved and is not asking again.
Did I understood wrong the documentation? Is there an easy way to do this, like a wild card for the intermediate folder structure? sales/*/in ? Thanks!
See Inbound Channel Adapters: Polling Multiple Servers and Directories.
Use a RotatingServerAdvice with a custom RotationPolicy.
Or use an outbound gateway instead, with a recursive MGET command.
https://docs.spring.io/spring-integration/docs/current/reference/html/sftp.html#sftp-outbound-gateway

Spring Integration Control Bus message to change selector of a JMS Inbound Channel Adapter

I'm currently implementing a flow on a Spring Integration-based (ver. 3.0.1.RELEASE) application that requires to store messages on a JMS queue to be picked up later.
For that, I've been trying to use a Spring Integration JMS Inbound Channel Adapter with a custom selector, and then picking up the message from the queue by changing the JMS selector of the JMSDestinationPollingSource to some matching ID included as a header property.
One of the requirements for this is that I cannot add a new service or a JAVA method, so I've been trying to sort it out using a Control Bus, but keep receiving the same error when I send the message to set the messageSelector to something different.
Inbound Channel Adapter definition:
<int-jms:inbound-channel-adapter id="inboundAdapter"
channel="inboundChannel"
destinationName="bufferQueue"
connection-factory="connectionFactory"
selector="matchingID = 'NO VALUE'">
<int:poller fixed-delay="1000"/>
</int-jms:inbound-channel-adapter>
Message:
#'inboundAdapter.source'.setMessageSelector("matchingID = 'VALUE'")
Error:
EvaluationException: The method 'public void org.springframework.integration.jms.JmsDestinationPollingSource.setMessageSelector(java.lang.String)' is not supported by this command processor. If usign the Control Bus, consider adding #ManagedOperation or #ManagedAttribute.
Which, AFAIK, means that the JmsDestinationPollingSource class is not Control Bus manageable, as it's not passing the ControlBusMethodFilter.
Is this approach nonviable, or is there something I'm missing? Is there any way to set the selector dynamically using SI XML configuration files only?
First of all it is strange to use Java tool and don't allow to write code on Java...
But that is your choice, or as you said requirements.
Change the employer! ;-)
That's correct: Control Bus allows only #ManagedOperation and #ManagedAttribute method. Since JmsDestinationPollingSource.setMessageSelector. We can make it like that. But does it make so much sense if we can reach it a bit different approach?
<int:outbound-channel-adapter id="changeSelectorChannel"
ref"inboundAdapter.source method="setMessageSelector"/>
where a new selector expression should be as a payload of the Message to this channel.

How to acknowledge message through program using Spring AMQP/Spring integration

1) Server sends a message to client.
2) Inbound channel adapter is configured to wait for "MANUAL" acknowledge mode operation from consumer
3) "TaskBundlereceiver" bean is implementing "ChannelAwareMessageListener" and in the implementation method, I am performing message acknowledgement.
I don't see "TaskBundlereceiver" getting executed. Am I missing something ?
Below is the configuration details of the steps that I have explained.
Appreciate your inputs.
#Override
public void onMessage(org.springframework.amqp.core.Message message, Channel channel) throws Exception
{
logger.debug("In onMessage method of the channel aware listener. message =["+message.getBody().toString()+"]");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
}
XML Configuration :
<!-- Channel that receives the task bundle from the server for execution -->
<int:channel id="fromKServerChannel"/>
<int-amqp:inbound-channel-adapter id="taskBundleReceiverAdapter"
channel="fromKServerChannel"
error-channel="taskBundleErrorChannel"
acknowledge-mode="MANUAL"
expose-listener-channel="true"
queue-names="kanga_task_queue"
connection-factory="connectionFactory"
concurrent-consumers="20"/>
<int:chain input-channel="fromKServerChannel" output-channel="nullChannel">
<int:service-activator ref="taskBundleReceiver" method="onMessage"/>
<int:service-activator ref="taskBundleExecutor" method="executeBundle"/>
</int:chain>
It doesn't work that way; the listener is the adapter, not the service invoked via the service-activator. The adapter currently does not support passing the channel to the client for manual acks. The expose-listener-channel attribute is for use when using transactions, so a down-stack rabbit template can participate in the transaction.
Why do you want MANUAL acks? AUTO (default) means the ack will be done automatically by the container when the thread returns normally; if your service throws an exception, the message will be nacked.
So, that's how to control the ack.
If you really want to use MANUAL acks, you'll have to use a <rabbit:listener-container/> to invoke your taskBundleReceiver directly. It could then send a message to the executor using a messaging gateway.

Resources