0

I'm trying to define a flow for a single-threaded handler. Messages come in great number and the handler is slow (it's inefficient to process them one by one). So I want to make the handler consume all messages available in the channel at once (or wait until a few messages accumulate) with Java DSL. If there are no messages in the channel and the handler has processed a previous group it should wait for a certain period of time (timeout "a") for a few messages to accumulate in the channel. But if messages keep coming, the handler MUST consume them after a certain period of time from the previous execution (timeout "b"). Therefore time intervals between handler executions should be no more than "b" (unless no messages arrive in the channel).

There is no reason to make multiple instances of that sort of handler: it generates data for interfaces. The code below describes some basic configuration. My problem is that I'm not able to come up with debouncing (the timeout "b") and releasing the group once the handler execution is completed.

    @Configuration
    public class SomeConfig {

        private AtomicBoolean someHandlerBusy = new AtomicBoolean(false);

        @Bean
        StandardIntegrationFlow someFlow() {
            return IntegrationFlows
                    .from("someChannel")
                    .aggregate(aggregatorSpec -> aggregatorSpec
                                    //The only rule to release a group:
                                    //wait 500ms after last message and have a free someHandler
                                    .groupTimeout(500)
                                    .sendPartialResultOnExpiry(true) //if 500ms expired - send group
                                    .expireGroupsUponCompletion(true) //group should be filled again
                                    .correlationStrategy(message -> true) //one group key, all messages in oe group
                                    .releaseStrategy(message -> false) //never release messages, only with timeout

                                    //Send messages one by one. This is not part of this task.
                                    //I just want to know how to do that. Like splitter.
                                    //.outputProcessor(MessageGroup::getMessages)
                    )
                    .handle("someHandler")
                    .get();
        }
    }

I have the solution with plain Java (kotlin) code: https://pastebin.com/mti3Y5tD


UPDATE

The configuration below does not erase group. The group is growing and growing and it falls wit error at the end.

Error:

*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844

Configuration:

    @Configuration
    public class InterfaceHandlerConfigJava {


        @Bean
        MessageChannel interfaceAggregatorFlowChannel() {
            return MessageChannels.publishSubscribe("interfaceAggregatorFlowChannel").get();
        }

        @EventListener(ApplicationReadyEvent.class)
        public void initTriggerPacket(ApplicationReadyEvent event) {
            MessageChannel channel = event.getApplicationContext().getBean("interfaceAggregatorFlowChannel", MessageChannel.class);
            channel.send(MessageBuilder.withPayload(new InterfaceHandler.HandlerReadyMessage()).build());
        }

        @Bean
        StandardIntegrationFlow someFlow(
                InterfaceHandler interfaceHandler
        ) {
            long lastMessageTimeout = 10L;
            return IntegrationFlows
                    .from("interfaceAggregatorFlowChannel")
                    .aggregate(aggregatorSpec -> aggregatorSpec
                            .groupTimeout(messageGroup -> {
                                if (haveInstance(messageGroup, InterfaceHandler.HandlerReadyMessage.class)) {
                                    System.out.println("case HandlerReadyMessage");
                                    if (haveInstance(messageGroup, DbChangeStreamConfiguration.InitFromDbMessage.class)) {
                                        System.out.println("case InitFromDbMessage");
                                        return 0L;
                                    } else if (messageGroup.size() > 1) {
                                        long groupCreationTimeout =
                                                messageGroup.getTimestamp() + 500L - System.currentTimeMillis();
                                        long timeout = Math.min(groupCreationTimeout, lastMessageTimeout);

                                        System.out.println("case messageGroup.size() > 1, timeout: " + timeout);
                                        return timeout;
                                    }
                                }
                                System.out.println("case Handler NOT ReadyMessage");
                                return null;
                            })
                            .sendPartialResultOnExpiry(true)
                            .expireGroupsUponCompletion(true)
                            .expireGroupsUponTimeout(true)
                            .correlationStrategy(message -> true)
                            .releaseStrategy(message -> false)
                    )
                    .handle(interfaceHandler, "handle")
                    .channel("interfaceAggregatorFlowChannel")
                    .get();
        }

        private boolean haveInstance(MessageGroup messageGroup, Class clazz) {
            for (Message<?> message : messageGroup.getMessages()) {
                if (clazz.isInstance(message.getPayload())) {
                    return true;
                }
            }
            return false;
        }
    }

I want to highlight: this flow is in the cycle. There is IN and no OUT. Messages go to the IN but handler emits HandlerReadyMessage at the end. Maybe there should be some thread breaker channel?


FINAL VARIANT

As aggregator and handler should not blocks each other and should not try to make a stackoverflow exception they should run in different threads. In the configuration above this achieved with queue channels. Looks that publish-subscribe channels are not running subscribers in different threads (at least for one subscriber).

    @Configuration
    public class InterfaceHandlerConfigJava {

        // acts as thread breaker too
        @Bean
        MessageChannel interfaceAggregatorFlowChannel() {
            return MessageChannels.queue("interfaceAggregatorFlowChannel").get();
        }

        @Bean
        MessageChannel threadBreaker() {
            return MessageChannels.queue("threadBreaker").get();
        }

        @EventListener(ApplicationReadyEvent.class)
        public void initTriggerPacket(ApplicationReadyEvent event) {
            MessageChannel channel = event.getApplicationContext().getBean("interfaceAggregatorFlowChannel", MessageChannel.class);
            channel.send(MessageBuilder.withPayload(new InterfaceHandler.HandlerReadyMessage()).build());
        }

        @Bean
        StandardIntegrationFlow someFlow(
                InterfaceHandler interfaceHandler
        ) {
            long lastMessageTimeout = 10L;
            return IntegrationFlows
                    .from("interfaceAggregatorFlowChannel")
                    .aggregate(aggregatorSpec -> aggregatorSpec
                            .groupTimeout(messageGroup -> {
                                if (haveInstance(messageGroup, InterfaceHandler.HandlerReadyMessage.class)) {
                                    System.out.println("case HandlerReadyMessage");
                                    if (haveInstance(messageGroup, DbChangeStreamConfiguration.InitFromDbMessage.class)) {
                                        System.out.println("case InitFromDbMessage");
                                        return 0L;
                                    } else if (messageGroup.size() > 1) {
                                        long groupCreationTimeout =
                                                messageGroup.getTimestamp() + 500L - System.currentTimeMillis();
                                        long timeout = Math.min(groupCreationTimeout, lastMessageTimeout);

                                        System.out.println("case messageGroup.size() > 1, timeout: " + timeout);
                                        return timeout;
                                    }
                                }
                                System.out.println("case Handler NOT ReadyMessage");
                                return null;
                            })
                            .sendPartialResultOnExpiry(true)
                            .expireGroupsUponCompletion(true)
                            .expireGroupsUponTimeout(true)
                            .correlationStrategy(message -> true)
                            .releaseStrategy(message -> false)
                            .poller(pollerFactory -> pollerFactory.fixedRate(1))
                    )
                    .channel("threadBreaker")
                    .handle(interfaceHandler, "handle", spec -> spec.poller(meta -> meta.fixedRate(1)))
                    .channel("interfaceAggregatorFlowChannel")
                    .get();
        }

        private boolean haveInstance(MessageGroup messageGroup, Class clazz) {
            for (Message<?> message : messageGroup.getMessages()) {
                if (clazz.isInstance(message.getPayload())) {
                    return true;
                }
            }
            return false;
        }
    }
Lewik
  • 649
  • 1
  • 6
  • 19

1 Answers1

0

It's not clear what you mean by timer b, but you can use a .groupTimeoutExpression(...) to dynamically determine the group timeout.

You don't need to worry about sending messages one by one; when the output processor returns a collection of Message<?> they are sent one-at-a-time.

Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • Timeout "a" - timespan after the last element of the group. Timeout "b" - timespan after the first element of the group. Thus a message group is all messages that arrive either within a timespan "b" or before a timeout "a" occurs. – Lewik Aug 06 '19 at 16:55
  • The `MessageGroupStoreReaper` (by default) will expire based on group creation time. The `groupTimeout` expires based on idle time. You can configure both to achieve your needs. [Reaper docs here](https://docs.spring.io/spring-integration/docs/5.1.7.RELEASE/reference/html/#reaper). It's a bit unusual to configure both, but I don't see why it wouldn't work. Presumably, b is less than a. – Gary Russell Aug 06 '19 at 17:16
  • Should it be more like " "a" is less than "b" "? I've just provided another version of the configuration. I didn't read about `MessageGroupStoreReaper` yet, I think there will be the same trouble regarding "how to tell Reaper that handler is ready". The new configuration should help to understand my goals and questions. – Lewik Aug 06 '19 at 17:31
  • Bear in mind that the groupTImeout function is only evaluated when a message arrives, so it won't really help with immediately releasing the group when the handler becomes idle. You could, of course, call `expireMessageGroups` on the message store from the handler - but you would probably want to do it on a different thread to avoid a potential stack overflow. You could use a custom output processor to mark the last message in the release so that the handler would know when to expire the next group. – Gary Russell Aug 06 '19 at 17:39
  • Could I make the handler send the special message to the aggregator input? And groupTimeout will be triggered by this message. – Lewik Aug 06 '19 at 17:43
  • Yes, that would work too (presumably with the handler ignoring that message when it comes around). – Gary Russell Aug 06 '19 at 17:44
  • The handler is ignoring that message. Or I can just transform the combined message to delete special messages. But, it's not working, could you take a look at the configuration, please? I think there should be some "thread breaker" channel. PublishSubscribe didn't help, maybe it has some optimization when working with one consumer? – Lewik Aug 06 '19 at 17:51
  • 1
    It looks ok, but you should return `1L` instead of `0L` - otherwise the release will run on the same thread and you could end up with a stack overflow. How does the handler know when to send a `HandlerReadyMessage`? – Gary Russell Aug 06 '19 at 18:01
  • This is an interesting trick with 1L. In the meantime, I tried queue channels and pollers (fixedRate(1)) as thread breakers (`interfaceAggregatorFlowChannel` and before `handle(interfaceHandler, "handle")`). Seems it works. I prefer implicitly channel names to show that there should be thread breakers. Handler knows when to send. At the end of the handle method, when a heavy job is done. It just returns `HandlerReadyMessage`. Is it ok to use two sets of "queue channels and pollers (fixedRate(1))" – Lewik Aug 06 '19 at 18:11
  • 1
    Oh; you are using the default outputProcessor, now, so the payload is a collection of payloads; with the custom processor in the original question you'll get discrete messages released. Yes, it's ok to do it that way - you won't be spinning a CPU since the default `receiveTimeout` in the queue channel is 1000. You'll effectively tie up a scheduler thread permanently though (there are 10 by default) so if you do it a lot you might need more scheduler threads. – Gary Russell Aug 06 '19 at 18:19