0

I have implemented the following scenario:

  1. A queueChannel holding Messages in form of byte[]
  2. A MessageHandler, polling the queue channel and uploading files over sftp
  3. A Transformer, listening to errorChannel and sending extracted payload from the failed message back to the queueChannel (thought of as an error handler to handle failed messages so nothing gets lost)

If the sftp server is online, everything works as expected.

If the sftp server is down, the errormessage, that arrives as the transformer is:

org.springframework.messaging.MessagingException: Failed to obtain pooled item; nested exception is java.lang.IllegalStateException: failed to create SFTP Session

The transformer cannot do anything with this, since the payload's failedMessage is null and throws an exception itself. The transformer looses the message.

How can I configure my flow to make the tranformer get the right message with the corresponding payload of the unsucsesfully uploaded file?

My Configuration:

  @Bean
  public MessageChannel toSftpChannel() {
    final QueueChannel channel = new QueueChannel();
    channel.setLoggingEnabled(true);
    return new QueueChannel();
  }

  @Bean
  public MessageChannel toSplitter() {
    return new PublishSubscribeChannel();
  }

  @Bean
  @ServiceActivator(inputChannel = "toSftpChannel", poller = @Poller(fixedDelay = "10000", maxMessagesPerPoll = "1"))
  public MessageHandler handler() {
    final SftpMessageHandler handler = new SftpMessageHandler(sftpSessionFactory());
    handler.setRemoteDirectoryExpression(new LiteralExpression(sftpRemoteDirectory));
    handler.setFileNameGenerator(message -> {
      if (message.getPayload() instanceof byte[]) {
        return (String) message.getHeaders().get("name");
      } else {
        throw new IllegalArgumentException("byte[] expected in Payload!");
      }
    });
    return handler;
  }

  @Bean
  public SessionFactory<LsEntry> sftpSessionFactory() {
    final DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);

    final Properties jschProps = new Properties();
    jschProps.put("StrictHostKeyChecking", "no");
    jschProps.put("PreferredAuthentications", "publickey,password");
    factory.setSessionConfig(jschProps);

    factory.setHost(sftpHost);
    factory.setPort(sftpPort);
    factory.setUser(sftpUser);
    if (sftpPrivateKey != null) {
      factory.setPrivateKey(sftpPrivateKey);
      factory.setPrivateKeyPassphrase(sftpPrivateKeyPassphrase);
    } else {
      factory.setPassword(sftpPasword);
    }
    factory.setAllowUnknownKeys(true);
    return new CachingSessionFactory<>(factory);
  }

  @Bean
  @Splitter(inputChannel = "toSplitter")
  public DmsDocumentMessageSplitter splitter() {
    final DmsDocumentMessageSplitter splitter = new DmsDocumentMessageSplitter();
    splitter.setOutputChannelName("toSftpChannel");
    return splitter;
  }

  @Transformer(inputChannel = "errorChannel", outputChannel = "toSftpChannel")
  public Message<?> errorChannelHandler(ErrorMessage errorMessage) throws RuntimeException {

    Message<?> failedMessage = ((MessagingException) errorMessage.getPayload())
      .getFailedMessage();
    return MessageBuilder.withPayload(failedMessage)
                         .copyHeadersIfAbsent(failedMessage.getHeaders())
                         .build();
  }

  @MessagingGateway 
  public interface UploadGateway {

    @Gateway(requestChannel = "toSplitter")
    void upload(@Payload List<byte[]> payload, @Header("header") DmsDocumentUploadRequestHeader header);
  }

Thanks..

Update

@Bean(PollerMetadata.DEFAULT_POLLER)
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
  PollerMetadata poller() {
    return Pollers
      .fixedRate(5000)
      .maxMessagesPerPoll(1)
      .receiveTimeout(500)
      .taskExecutor(taskExecutor())
      .transactionSynchronizationFactory(transactionSynchronizationFactory())
      .get();
  }

  @Bean
  @ServiceActivator(inputChannel = "toMessageStore", poller = @Poller(PollerMetadata.DEFAULT_POLLER))
  public BridgeHandler bridge() {
    BridgeHandler bridgeHandler = new BridgeHandler();
    bridgeHandler.setOutputChannelName("toSftpChannel");
    return bridgeHandler;
  }
  • Sorry, I thought you were polling an inbound channel adapter. Turn on DEBUG logging and follow the message flow. If you still can't figure it out, post the log someplace. – Gary Russell Mar 01 '18 at 21:45
  • You also probably shouldn't be using a `QueueChannel` if you are worried about message loss. – Gary Russell Mar 01 '18 at 21:46
  • @gary Hello Gary, thanks for your reply... I am mostly worried that the failed upload will result in file loss.. in my understanding I have to queue the files somehow/somewhere to be able to retry the upload... Or is there a better way for handling such use cases? Here is the log [link](https://www.dropbox.com/s/i560kabwyhmrntm/si.log?dl=0) – Rok Purkeljc Mar 02 '18 at 07:42
  • Please see my answer. – Gary Russell Mar 02 '18 at 14:29

1 Answers1

1

The null failedMessage is a bug; reproduced INT-4421.

I would not recommend using a QueueChannel for this scenario. If you use a direct channel, you can configure a retry advice to attempt redeliveries. when the retries are exhausted (if so configured), the exception will be thrown back to the calling thread.

Add the advice to the SftpMessageHandler's adviceChain property.

EDIT

You can work around the "missing" failedMessage by inserting a bridge between the pollable channel and the sftp adapter:

@Bean
@ServiceActivator(inputChannel = "toSftpChannel", poller = @Poller(fixedDelay = "5000", maxMessagesPerPoll = "1"))
public BridgeHandler bridge() {
    BridgeHandler bridgeHandler = new BridgeHandler();
    bridgeHandler.setOutputChannelName("toRealSftpChannel");
    return bridgeHandler;
}

@Bean
@ServiceActivator(inputChannel = "toRealSftpChannel")
public MessageHandler handler() {
    final SftpMessageHandler handler = new SftpMessageHandler(sftpSessionFactory());
    handler.setRemoteDirectoryExpression(new LiteralExpression("foo"));
    handler.setFileNameGenerator(message -> {
        if (message.getPayload() instanceof byte[]) {
            return (String) message.getHeaders().get("name");
        }
        else {
            throw new IllegalArgumentException("byte[] expected in Payload!");
        }
    });
    return handler;
}
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • You can use `QueueChannel` as long as you also use a transactional `ChannelMessageStore`, e.g. `JdbcChannelMessageStore`. In this case when an exception is thrown after retry is exhausted, the transaction will be rolled back and the message will retain in the store. – Artem Bilan Mar 02 '18 at 14:10
  • @ArtemBilan: could you please provide some example, how to set up a QueueChannel with a RedisChannelPriorityMessageStore with Java Annotations? – Rok Purkeljc Mar 03 '18 at 15:32
  • @RokPurkeljc There's a java config example [in the documentation](https://docs.spring.io/spring-integration/reference/html/messaging-channels-section.html#channel-configuration-queuechannel) with a mongo store. Also see my edit for a work around for the `null` `failedMessage`. – Gary Russell Mar 03 '18 at 15:43
  • Redis is not transactional, so, no guarantee that you won’t lose message in case of exception – Artem Bilan Mar 03 '18 at 17:38
  • Tried to setup a JdbcChannelMessageStore with an Oracle Datasource. How can I enable the @Transactional on my custom (using PollerMetadata) defined Poller with Java Annotations (see Update)? – Rok Purkeljc Mar 05 '18 at 12:36
  • @ArtemBilan - i Think the "@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)" on the PollerMetadata isn't working... – Rok Purkeljc Mar 05 '18 at 12:42
  • No, those two are not related. `Metadata` is not an active object – Artem Bilan Mar 05 '18 at 14:34
  • So are there any possibilities to use a @Transactional Annotation on a Poller defined with PollerMetadata? – Rok Purkeljc Mar 05 '18 at 15:08