Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to test events from other threads? #1074

Open
jekutzsche opened this issue Feb 22, 2025 · 10 comments
Open

How to test events from other threads? #1074

jekutzsche opened this issue Feb 22, 2025 · 10 comments
Assignees
Labels
in: test support Spring Boot integration testing meta: waiting for feedback Waiting for feedback of the original reporter

Comments

@jekutzsche
Copy link

Hi, I am testing the response to events using org.springframework.modulith.test.Scenario. The test publishes an event, my code processes it and should again publish events that the test checks for. The processing and publishing of the new events should be done asynchronously using JobRunr.

If events are now published via the ApplicationEventPublisher within a separate thread (the JobRunr job), then Scenario.andWaitForEventOfType does not react to this in the tests.

If I have found this out correctly, it is because the ThreadBoundApplicationListenerAdapter used by the PublishedEventsParameterResolver has not registered a listener for the JobRunr thread. A listener is only registered for the main thread in the test. I now assume that it works with the threads of @Async (the test works there without any problems, although it is also a different thread) because they inherit from Main. However, this does not seem to be the case with JobRunr threads.

From the comments on ThreadBoundApplicationListenerAdapter, I assume that this behavior is intended. However, I have not found a way to wait correctly for the events from the JobRunr threads. What is the concept behind this and what is the best way to implement the tests?

@odrotbohm
Copy link
Member

I am neither a JobRunr or Awaitility expert, but the When stage of a Scenario pipeline (what you get back from Scenario.stimulate(…) or ….publish(…) exposes a customize(Function<ConditionFactory, …>) which is essentially the Awaitility execution infrastructure. That in turn exposes a pollThread(…) method that allows to define the Thread that should be inspected for results.

Alternatively — and I realize I might be fully off as I don't have the slightest clue about JobRunr —, as TBALA uses an InheritableThreadLocal, maybe you can set up to create threads as children of the originating one? The asynchronous method invocation in Spring does exactly that.

@odrotbohm odrotbohm self-assigned this Feb 23, 2025
@odrotbohm odrotbohm added in: test support Spring Boot integration testing meta: waiting for feedback Waiting for feedback of the original reporter labels Feb 23, 2025
@jekutzsche
Copy link
Author

Hi Ollie, thank you very much for your answer and your efforts.

I am neither a JobRunr or Awaitility expert, but the When stage of a Scenario pipeline (what you get back from Scenario.stimulate(…) or ….publish(…) exposes a customize(Function<ConditionFactory, …>) which is essentially the Awaitility execution infrastructure. That in turn exposes a pollThread(…) method that allows to define the Thread that should be inspected for results.

Unfortunately, I don't see how the pollThread(...) method can help me here, as Awaitility is not yet involved at this point.

The Scenario is created by the ScenarioParameterResolver, which in turn generates the internally used AssertablePublishedEvents with the PublishedEventsParameterResolver. This is used for the Awaitility condition.

However, this AssertablePublishedEvents is registered by PublishedEventsParameterResolver.resolveParameter as a delegate with TBALA, which is created beforehand and registered with the AbstractApplicationContext as a listener. As the AssertablePublishedEvents is registered with the thread with which the test is created, no events from other threads reach it. Awaitility cannot then detect an event, regardless of which thread it polls with.

I hope I have recognized and understood the connections correctly.

I don't have an easy way to get to the job's thread. From my point of view, it would make sense if all events reach the AssertablePublishedEvents regardless of the thread in which they were published.

Alternatively — and I realize I might be fully off as I don't have the slightest clue about JobRunr —, as TBALA uses an InheritableThreadLocal, maybe you can set up to create threads as children of the originating one? The asynchronous method invocation in Spring does exactly that.

I had that thought too, but I haven't found an easy way to control the thread creation.

At the moment I am making do with:

@ApplicationModuleTest
@Import(TestEventListener.class)
@RequiredArgsConstructor
class XxxIT {

    private final TestEventListener listener;

    @Test
    void bootstrapsContainer(Scenario scenario) {

        scenario.publish(XxxEvent1.of())
                .andWaitAtMost(Duration.ofSeconds(60))
                .andWaitForStateChange(listener::isHandled)
                .andVerify(it -> {
                    assertThat(it).isTrue();
                });
    }

    static class TestEventListener {

        @Getter
        boolean handled;

        @ApplicationModuleListener
        void handle(XxxEvent2 event) {
            handled = true;
        }
    }
}

@odrotbohm
Copy link
Member

I don't quite understand how JobRunr plays into this yet. Maybe we can get @rdehuyss' input on this, how one would set it up to align with a Spring provided threading arrangement?

@jekutzsche
Copy link
Author

In my opinion, JobRunr does not play a special role here, but is just an example of when events are published from an independent thread. JobRunr now happens to be what I chose.

I would find it a bit strange if I had to adapt the productive code (to which the framework belongs) so that the test framework works with it. Here I would rather expect flexibility and customization in the test framework.

@jekutzsche
Copy link
Author

How is the comment on the ThreadBoundApplicationListenerAdapter class to be understood?

	 * {@link ThreadLocal} and get used on {@link #onApplicationEvent(ApplicationEvent)} if one is registered for the
	 * current thread. This allows multiple event listeners to see the events fired in a certain thread in a concurrent
	 * execution scenario.

I can't see how you could register multiple event listeners and somehow check for specific threads? Or does the comment only refer to the Modulith internal implementation?

@odrotbohm
Copy link
Member

The way the async event listener invocations work is the following. The executing thread (the one running your test) spawns new threads to then execute the listener methods on (that's how @Async works). TBALA uses an InheritableThreadLocal to make sure to see events published on child threads, in other words, from within listeners in turn. Let's say you have an event A published, handled by an @ApplicationModuleListener. The latter publishes a second event B to, for example, signal it is done processing the original event. That event B is then published on a thread spawned from the original one. ITL makes sure we also see B.

Zooming out a bit again, Scenario and AssertableApplicationEvents “see” events published on such a cascade of events and asynchronous handlers. I don't have a clue at all what

The processing and publishing of the new events should be done asynchronously using JobRunr.

means, in this context, how JobRunr handles threads and how they're connected to the thread executing the tests.

From my point of view, it would make sense if all events reach the AssertablePublishedEvents regardless of the thread in which they were published.

I don't think we can do that, as ApplicationContexts are cached between executions and might be in parallel use for concurrently executed tests.

@jekutzsche
Copy link
Author

Thank you for the detailed explanation.

I don't have a clue at all what

The processing and publishing of the new events should be done asynchronously using JobRunr.

means, in this context, how JobRunr handles threads and how they're connected to the thread executing the tests.

Please excuse the inaccuracy. I just wanted to say that in my code a method with @ApplicationModuleListener reacts to event A, creates and starts a job with JobRunr and the whole processing and especially the publishing of event B is done within the independent thread of JobRunr.

The threads created by JobRunr do not appear to be connected to the thread that executes the test. JobRunr persists the jobs and then probably executes them with a separate execution unit. The execution could also take place after a shutdown. For this reason, it seems logical to me that the threads are independent.

From my point of view, it would make sense if all events reach the AssertablePublishedEvents regardless of the thread in which they were published.

I don't think we can do that, as ApplicationContexts are cached between executions and might be in parallel use for concurrently executed tests.

Ahh, now a light is dawning on me and I understand the connections around the TBALA. Thank you for the clarification.

With these contexts in mind, the solution with the “auxiliary listener” seems to me to be the most pragmatic.

@rdehuyss
Copy link

Hi @odrotbohm, @jekutzsche - I'm reading through this thread but I don't know how I can be of any assistance?

To give a bit of context of how JobRunr works: it has it's own managed thread pool and just takes jobs of the queue to process and runs them on the threads of the thread pool. There is indeed no link (except for the MDC) to the original thread that created the job.

@jekutzsche
Copy link
Author

Hi @rdehuyss, thank you for your explanations. I now at least understand the background.

@jekutzsche
Copy link
Author

@odrotbohm I think it would be good if the documentation and the JavaDocs pointed out the pitfall discussed here. I can't imagine that checking for events from independent threads is a rare side issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: test support Spring Boot integration testing meta: waiting for feedback Waiting for feedback of the original reporter
Projects
None yet
Development

No branches or pull requests

3 participants