Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/antora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ asciidoc:
TestInstancePreDestroyCallback: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestInstancePreDestroyCallback.html[TestInstancePreDestroyCallback]'
TestTemplateInvocationContext: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestTemplateInvocationContext.html[TestTemplateInvocationContext]'
TestTemplateInvocationContextProvider: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.html[TestTemplateInvocationContextProvider]'
TestMethodReturnValueHandler: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestMethodReturnValueHandler.html[TestMethodReturnValueHandler]'
TestWatcher: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestWatcher.html[TestWatcher]'
PreInterruptCallback: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/PreInterruptCallback.html[PreInterruptCallback]'
# Jupiter Conditions
Expand Down
1 change: 1 addition & 0 deletions documentation/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
** xref:extensions/exception-handling.adoc[]
** xref:extensions/pre-interrupt-callback.adoc[]
** xref:extensions/intercepting-invocations.adoc[]
** xref:extensions/test-method-return-value-handling.adoc[]
** xref:extensions/providing-invocation-contexts-for-class-templates.adoc[]
** xref:extensions/providing-invocation-contexts-for-test-templates.adoc[]
** xref:extensions/keeping-state-in-extensions.adoc[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
= Test Method Return Value Handling

`{TestMethodReturnValueHandler}` defines an extension point that allows `@Test` methods
to return non-void types. By default, JUnit Jupiter requires `@Test` methods to return
`void`. A registered `TestMethodReturnValueHandler` relaxes this requirement for return
types it supports.

[[registration]]
== Registration

Implementations are discovered via Java's `{ServiceLoader}` mechanism and must be
registered in a file named
`META-INF/services/org.junit.jupiter.api.extension.TestMethodReturnValueHandler` on the
classpath.

[[how-it-works]]
== How It Works

A `TestMethodReturnValueHandler` has two responsibilities:

1. **Declare supported return types** via `supportsReturnType(Class<?>)` — during
discovery, JUnit calls this method to decide whether a non-void `@Test` method should
be accepted.

2. **Execute the test method** via `execute(Invocation<Object>, ExtensionContext)` — at
execution time, the handler receives an `{InvocationInterceptor}.Invocation` that wraps
the test method call. The handler must call `invocation.proceed()` to invoke the test
and obtain the return value. It is then responsible for processing the result — for
example, subscribing to a reactive type and awaiting completion.

Because the handler controls _when_ `proceed()` is called, it can set up a custom
execution context beforehand — for example, running the test on a specific thread or
executor.

[[example]]
== Example

The following handler adds support for `@Test` methods that return `CompletableFuture`:

[source,java,indent=0]
----
public class CompletableFutureHandler implements TestMethodReturnValueHandler {

@Override
public boolean supportsReturnType(Class<?> returnType) {
return CompletableFuture.class.isAssignableFrom(returnType);
}

@Override
public void execute(InvocationInterceptor.Invocation<Object> invocation,
ExtensionContext context) throws Throwable {
Object result = invocation.proceed();
if (result != null) {
((CompletableFuture<?>) result).get(30, TimeUnit.SECONDS);
}
}
}
----

With this handler registered, the following test is discovered and executed:

[source,java,indent=0]
----
@Test
CompletableFuture<String> asyncTest() {
return CompletableFuture.supplyAsync(() -> {
// test logic
return "result";
});
}
----
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ repository on GitHub.
and `@DisabledOnOs`.
* Failures caused by `@Timeout` expirations now include a hint about enabling
xref:writing-tests/timeouts.adoc#debugging-thread-dump[thread dumps].
* Added `TestMethodReturnValueHandler` extension point that allows `@Test` methods to
return non-void types. Handlers are discovered via Java's `ServiceLoader` mechanism and
can control test method invocation — for example, to execute the test on a specific
thread or executor and then process the return value (e.g. awaiting a reactive type).
See xref:extensions/test-method-return-value-handling.adoc[] for details.


[[v6.2.0-M1-junit-vintage]]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2015-2026 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api.extension;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import org.apiguardian.api.API;

/**
* {@code TestMethodReturnValueHandler} allows extensions to support
* {@link org.junit.jupiter.api.Test @Test} methods that return a non-void
* value.
*
* <p>By default, JUnit Jupiter requires {@code @Test} methods to return
* {@code void}. A registered {@code TestMethodReturnValueHandler} relaxes
* this requirement for return types it {@linkplain #supportsReturnType
* supports}. The handler is responsible for invoking the test method
* (via {@link InvocationInterceptor.Invocation#proceed()
* invocation.proceed()}) and processing the return value.
*
* <p>Because the handler controls the invocation, it can set up a
* custom execution context before calling {@code proceed()} &mdash; for
* example, running the test method on a specific thread or executor.
*
* <p>Implementations are discovered via Java's {@link java.util.ServiceLoader}
* mechanism and must be registered in
* {@code META-INF/services/org.junit.jupiter.api.extension.TestMethodReturnValueHandler}.
*
* <h2>Example</h2>
* <pre>{@code
* public class CompletableFutureHandler implements TestMethodReturnValueHandler {
*
* @Override
* public boolean supportsReturnType(Class<?> returnType) {
* return CompletableFuture.class.isAssignableFrom(returnType);
* }
*
* @Override
* public void execute(InvocationInterceptor.Invocation<Object> invocation,
* ExtensionContext context) throws Throwable {
* Object result = invocation.proceed();
* if (result != null) {
* ((CompletableFuture<?>) result).get(30, TimeUnit.SECONDS);
* }
* }
* }
* }</pre>
*
* @since 6.2
* @see org.junit.jupiter.api.Test
*/
@API(status = EXPERIMENTAL, since = "6.2")
public interface TestMethodReturnValueHandler extends Extension {

/**
* Determine if this handler supports the supplied return type.
*
* @param returnType the return type of the test method; never {@code null}
* @return {@code true} if this handler can handle the return type
*/
boolean supportsReturnType(Class<?> returnType);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use Type instead of Class<?>?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be more versatile, for sure. I can't think of any reason why anybody would need more than the raw type to determine whether it's reactive or not, but other use-cases might benefit from this. It would probably be wise to also make that Type available somehow from execute() (unless it's already present via the parameters somehow).


/**
* Execute a {@code @Test} method that returns a supported type.
*
* <p>The handler must call {@link InvocationInterceptor.Invocation#proceed()
* invocation.proceed()} to invoke the test method and obtain the return
* value. The handler is then responsible for processing the result
* &mdash; for example, subscribing to a reactive type and awaiting
* completion.
*
* <p>Because the handler controls when {@code proceed()} is called, it
* can set up a custom execution context beforehand &mdash; for example,
* running the test on a specific thread or executor.
*
* <p>If this method throws, the test is marked as failed with the thrown
* exception.
*
* @param invocation the invocation that executes the test method;
* calling {@code proceed()} invokes the method and returns its result;
* never {@code null}
* @param context the current extension context; never {@code null}
* @throws Throwable if the return value indicates a test failure
*/
void execute(InvocationInterceptor.Invocation<Object> invocation, ExtensionContext context) throws Throwable;

}
1 change: 1 addition & 0 deletions junit-jupiter-engine/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
requires org.opentest4j;

uses org.junit.jupiter.api.extension.Extension;
uses org.junit.jupiter.api.extension.TestMethodReturnValueHandler;

provides org.junit.platform.engine.TestEngine
with org.junit.jupiter.engine.JupiterTestEngine;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@
import java.util.function.UnaryOperator;

import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation;
import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler;
import org.junit.jupiter.api.extension.TestMethodReturnValueHandler;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import org.junit.jupiter.api.extension.TestInstancePreDestroyCallback;
import org.junit.jupiter.api.extension.TestInstances;
Expand All @@ -39,10 +42,12 @@
import org.junit.jupiter.engine.execution.AfterEachMethodAdapter;
import org.junit.jupiter.engine.execution.BeforeEachMethodAdapter;
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker;
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.ReflectiveInterceptorCall;
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.ReflectiveInterceptorCall.VoidMethodInterceptorCall;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.jupiter.engine.extension.MutableExtensionRegistry;
import org.junit.jupiter.engine.support.MethodReflectionUtils;
import org.junit.platform.commons.util.UnrecoverableExceptions;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestExecutionResult;
Expand Down Expand Up @@ -216,8 +221,15 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't it be reversed so be reactive by default to enable really concurrent method execution instead of reactive/async impl being made blocking?

side note: virtual threads can help but would also hide issues when not used in the enclosing environment so are not an answer to that need alone

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand. What should be reversed?

Method testMethod = getTestMethod();
Object instance = extensionContext.getRequiredTestInstance();
executableInvoker.invokeVoid(testMethod, instance, extensionContext, context.getExtensionRegistry(),
interceptorCall);
TestMethodReturnValueHandler handler = MethodReflectionUtils.findReturnValueHandler(testMethod);
if (handler != null) {
handler.execute(invokeTestMethodCapturingResult(testMethod, instance,
extensionContext, context.getExtensionRegistry()), extensionContext);
}
else {
executableInvoker.invokeVoid(testMethod, instance, extensionContext,
context.getExtensionRegistry(), interceptorCall);
}
}
catch (Throwable throwable) {
UnrecoverableExceptions.rethrowIfUnrecoverable(throwable);
Expand All @@ -226,6 +238,33 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe
});
}

@SuppressWarnings("NullAway")
private InvocationInterceptor.Invocation<Object> invokeTestMethodCapturingResult(Method testMethod, Object instance,
ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) {
return () -> executableInvoker.<@Nullable Object> invoke(testMethod, instance,
extensionContext, extensionRegistry, returnValueCapturingInterceptorCall());
}

@SuppressWarnings("NullAway")
private static ReflectiveInterceptorCall<Method, @Nullable Object> returnValueCapturingInterceptorCall() {
return (interceptor, invocation, invocationContext, extensionContext) -> {
Object[] resultHolder = new Object[1];
interceptor.interceptTestMethod(new InvocationInterceptor.Invocation<@Nullable Void>() {
@Override
public @Nullable Void proceed() throws Throwable {
resultHolder[0] = invocation.proceed();
return null;
}

@Override
public void skip() {
invocation.skip();
}
}, invocationContext, extensionContext);
return resultHolder[0];
};
}

private void invokeTestExecutionExceptionHandlers(ExtensionRegistry registry, ExtensionContext context,
Throwable throwable) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated;
import static org.junit.platform.commons.support.ModifierSupport.isNotAbstract;

import org.junit.jupiter.engine.support.MethodReflectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.function.BiFunction;
Expand Down Expand Up @@ -65,7 +67,9 @@ private static Condition<Method> isNotPrivate(Class<? extends Annotation> annota

protected static Condition<Method> hasVoidReturnType(Class<? extends Annotation> annotationType,
DiscoveryIssueReporter issueReporter) {
return issueReporter.createReportingCondition(method -> getReturnType(method) == void.class,
return issueReporter.createReportingCondition(
method -> getReturnType(method) == void.class
|| MethodReflectionUtils.hasReturnValueHandler(method),
method -> createIssue(annotationType, method, "must not return a value"));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,41 @@
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.ServiceLoader;

import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.extension.TestMethodReturnValueHandler;
import org.junit.platform.commons.support.ReflectionSupport;
import org.junit.platform.commons.util.ClassLoaderUtils;
import org.junit.platform.commons.util.KotlinReflectionUtils;

@API(status = INTERNAL, since = "6.0")
public class MethodReflectionUtils {

private static List<TestMethodReturnValueHandler> getReturnValueHandlers() {
return ServiceLoader //
.load(TestMethodReturnValueHandler.class, ClassLoaderUtils.getDefaultClassLoader()) //
.stream() //
.map(ServiceLoader.Provider::get) //
.toList();
}

public static boolean hasReturnValueHandler(Method method) {
Class<?> returnType = method.getReturnType();
return returnType != void.class
&& getReturnValueHandlers().stream().anyMatch(h -> h.supportsReturnType(returnType));
}

public static @Nullable TestMethodReturnValueHandler findReturnValueHandler(Method method) {
Class<?> returnType = method.getReturnType();
return getReturnValueHandlers().stream() //
.filter(h -> h.supportsReturnType(returnType)) //
.findFirst() //
.orElse(null);
}

public static Class<?> getReturnType(Method method) {
return isKotlinSuspendingFunction(method) //
? getKotlinSuspendingFunctionReturnType(method) //
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2015-2026 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.engine;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.TestMethodReturnValueHandler;

public class CompletableFutureReturnValueHandler implements TestMethodReturnValueHandler {

@Override
public boolean supportsReturnType(Class<?> returnType) {
return CompletableFuture.class.isAssignableFrom(returnType);
}

@Override
public void execute(InvocationInterceptor.Invocation<Object> invocation, ExtensionContext context) throws Throwable {
Object result = invocation.proceed();
if (result == null) {
return;
}
try {
((CompletableFuture<?>) result).get(30, TimeUnit.SECONDS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should likely be infinite or respect @timeout on the method but not 30s by default IMHO

as mentionned before it would be better to ensure the execution is reactive (not blocking there)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of making not void method void friendly, make void methods reactive friendly (CompletionStage can be a neutral contract), this way you can just run all reactive tests concurrently - respect lock semantic indeed.

the PR proposal is to make reactive method blocking so just enables syntaxic sugar but not a paradigm change/enablement.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a test, not an implementation, but otherwise I agree. Perhaps the API should make sure we can access any @Timeout on the method.

As for the execution having to block, I'm afraid that's pretty much forced, at least in the case of an event-loop reactive implementation such as Vert.x and Quarkus, because the tests are run on platform threads, and the test body has to be delegated to the event loop, so we cannot avoid delegation and blocking, but we only block the test thread, the test method is properly non-blocking.

I definitely don't mean to add support for async/reactive in JUnit, that would be a much bigger endeavour. Not one I would recommend, and definitely not just for this feature.

@rmannibucau rmannibucau Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for the execution having to block, I'm afraid that's pretty much forced, at least in the case of an event-loop reactive implementation such as Vert.x and Quarkus, because the tests are run on platform threads, and the test body has to be delegated to the event loop, so we cannot avoid delegation and blocking, but we only block the test thread, the test method is properly non-blocking.

no, the idea is that junit retrieve a CompletionStage<?> as result and calls handle on this stage so literally

try {
  execute();
  onSuccess();
} catch (...) {
  onError();
}

is replaced by

execute().handle((ok, ko) -> { if (ko != null) onError(ko); else onSuccess(); });

this way the test execution becomes reactive, indeed the "void" (current) execute will be blocking as of today:

final var result = new CompletableFuture<Void>();
try {
  execute();
  result.complete(null);
} catch (final Throwable ex) {
  result.completeExceptionally(ex);
}
return result;

TIP: it can use a dedicated thread pool ;)

so literally you can bridge any reactive or not framework and have a fully reactive execution (just take care of thread pools to not lock but this can be a detail of the SPI impl)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most cases I would not want to run reactive tests any more concurrently than blocking tests. Most of my tests have cleanup/teardown that would break if run concurrently. That's not the point of reactive tests.

IMO the only point is to test APIs that are reactive in a way that is natural for people writing reactive tests, rather than rely on syntax that is workaround-specific.

But true, given a properly parallelisable test suite, it might require something different to be able to collect all reactive tests and run them in parallel. It would be different than how blocking tests run in parallel, because we could just compose all the tests and block on a single thread, rather than start a thread pool.

I was hoping this kind of support could come later, as I don't need it and would not know how to achieve it, and this initial support could be a good first step, as it would open the door to tests that return a reactive type, and later if someone contributes support for more efficient parallelising of reactive tests, the syntax from the PoV of the users would not have to change: you can already return reactive types. But the SPI might have to change then, in ways I'm unsure about without experimentation/research.

In general I prefer to have something valuable now and small than something perfect later, but that's up to you, I can understand both points of view 😅

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute().handle((ok, ko) -> { if (ko != null) onError(ko); else onSuccess(); });

Most frameworks not written with async in mind won't support this easily. I am not familiar with the insides of JUnit, so probably you know better, but I would assume this would be hard to do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most cases I would not want to run reactive tests any more concurrently than blocking tests. Most of my tests have cleanup/teardown that would break if run concurrently. That's not the point of reactive tests.

🤔 it is in the original ticket - and is exatly the reference to https://docs.nunit.org/articles/nunit/writing-tests/attributes/test.html or https://learn.microsoft.com/en-us/archive/msdn-magazine/2014/november/async-programming-unit-testing-asynchronous-code for ex. Else you just save a .get() from your test body which is already doable through 3-4 other ways (like having an await helper like awaitability or a parameter swallowing the promise to await it synchronously or through a junit interceptor).

Indeed your current tests will be broken but it will also require some rewriting. Side note: it is already the case since junit supports concurrent test execution on half of the related aspects so nothing new there.

Most frameworks not written with async in mind won't support this easily.

while it is wrapped it will not be worse than today and threading model is controlled by junit so think it will be ~the same no?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 it is in the original ticket - and is exatly the reference to https://docs.nunit.org/articles/nunit/writing-tests/attributes/test.html or https://learn.microsoft.com/en-us/archive/msdn-magazine/2014/november/async-programming-unit-testing-asynchronous-code for ex.

These are for programming languages that are not Java 🤔

Else you just save a .get() from your test body which is already doable through 3-4 other ways (like having an await helper like awaitability or a parameter swallowing the promise to await it synchronously or through a junit interceptor).

I did mention we currently achieve this via a parameter and a style that is not reactive-friendly. This is not just throwing a .get() as I mentioned we also need to offload the test method onto the event-loop. I don't think we tried using a JUnit interceptor, but given that void methods are currently banned, I don't think it would work.

And BTW, I don't think just because a test is reactive it should be run in parallel, I think that the same rule should apply as for blocking methods wrt. running them in parallel or not.

while it is wrapped it will not be worse than today and threading model is controlled by junit so think it will be ~the same no?

I would defer to the JUnit experts to answer that. Given that they support parallel tests, it should be doable. Not necessarily easy. It was definitely beyond the scope of my PR, though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.net is quite aligned on "modern" (java >= 8) java on that aspect, java ecosystem just never payed the bill (likely cause it is split compared to MS ecosystem - blind guess).

And BTW, I don't think just because a test is reactive it should be run in parallel, I think that the same rule should apply as for blocking methods wrt. running them in parallel or not.

this is where this programming model shines, you do decide since the caller just let you hande the context (thread) you do use, so all good and it embraces your statement 100% IMHO

}
catch (ExecutionException ex) {
throw ex.getCause();
}
catch (TimeoutException ex) {
throw new AssertionError("CompletableFuture did not complete within 30 seconds", ex);
}
}

}
Loading