Skip to content

Introduce necessary executors and implement sleepAsync#3

Draft
stIncMale wants to merge 3 commits into
introduceRetryPolicyfrom
sleepAsync
Draft

Introduce necessary executors and implement sleepAsync#3
stIncMale wants to merge 3 commits into
introduceRetryPolicyfrom
sleepAsync

Conversation

@stIncMale

@stIncMale stIncMale commented Jun 30, 2026

Copy link
Copy Markdown
Owner

AI usage

AI was used only to review and to suggest ways to deal with the serious bug it discovered (see below).

AI identified a serious bug with CommonExecutor offloading scheduled tasks to another Executor, which I failed to think about on my own. AI also expressed ideas on how one may deal with that problem. One of them I manually implemented in DefaultAsyncClientExecutor.

JAVA-6240

@stIncMale stIncMale self-assigned this Jun 30, 2026
@stIncMale
stIncMale force-pushed the sleepAsync branch 5 times, most recently from 634c8dd to 85b1c3d Compare July 3, 2026 07:39
return asyncFunctionSuccessfulResult.get().getNullable();
}

private void sleep(final Duration duration) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VAKOTODO Make static.

public void onResult(@Nullable final R attemptSuccessfulResult, @Nullable final Throwable attemptFailedResult) {
if (attemptFailedResult != null) {
MutableValue<MutableValue<R>> asyncFunctionSuccessfulResult = new MutableValue<>();
beginAsync().thenRunWhileLoop(() -> asyncFunctionSuccessfulResult.getNullable() == null, iterationCallback -> {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VAKOTODO thenRunWhileLoop ends if the body fails. Rewrite taking this into account.

return thenRun(callback -> {
new RetryingAsyncCallbackSupplier<Void>(
// `AsyncClientExecutor` is not needed, given the contract of `SimpleRetryPolicy`, `RetryingAsyncCallbackSupplier`
AsyncClientExecutor.unimplemented(),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VAKOTODO Replace AsyncClientExecutor.unimplemented with AsyncClientExecutor.NO_OP.

@Override
protected void afterExecute(final Runnable r, @Nullable final Throwable t) {
super.afterExecute(r, t);
assertTrue(r instanceof Future<?>);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VAKOTODO Leave a comment explaining this assertion.

commonExecutor().uncaughtError((Error) exception);
}
if (exception != null) {
logger.error("A task completed abruptly", exception);

@stIncMale stIncMale Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VAKOTODO If t != null (and so is exception), then conceptually do commonExecutor().uncaughtError(new AssertionError(exception)), but make sure not to double-wrap AssertionError. This way Logger is not needed at all.

Leave a comment about uncaughtError logging to stdout in there is no better action, as documented by ThreadGroup.uncaughtException.

Discuss this with @vbabanin again. Given that one of the motivations behind overriding afterExecute was to do it "Instead of modifying every Runnable/Callable we schedule", maybe we should not treat uncaught Exceptions as bugs.

@stIncMale stIncMale Jul 21, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Discussed, we will treat Exceptions in afterExecute as AssertionError, because this gives an application a standard programmatic way to react to a driver bug when asynchronous driver API is used with MongoThreadPoolExecutor being used as the IO executor.

Update the root AGENTS.md with the new Code correctness rules section that explains the rule for all tasks that run in MongoThreadPoolExecutor/MongoScheduledThreadPoolExecutor.

return assertNotNull(e.getCause());
} catch (InterruptedException e) {
// not else to do but to reinstate the interrupted status
Thread.currentThread().interrupt();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VAKOTODO
Do

                try {
                    return getException(r, t);
                } finally {
                    // not else to do but to reinstate the interrupted status
                    Thread.currentThread().interrupt();
                }

This way we get the exception even if the thread was interrupted. There is no harm, only benefits.

* All {@link Throwable}s are logged.</li>
* </ul>
*/
public final class MongoThreadPoolExecutor extends ThreadPoolExecutor {

@stIncMale stIncMale Jul 21, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VAKOTODO Update the description of https://jira.mongodb.org/browse/JAVA-6109: mention that all other executor implementations / single threads should be replaces either with virtual threads (https://jira.mongodb.org/browse/JAVA-4930 - distant future), or MongoThreadPoolExecutor/MongoScheduledThreadPoolExecutor (this includes the executors created in AsynchronousTlsChannelGroup, NettyStreamFactoryFactory (NioEventLoopGroup)), unless it's an IO executor supplied by an application.

Also mention in that ticket to document that the executors supplied by applications should themselves make sure uncaught Throwables are not swallowed, potentially the same way MongoThreadPoolExecutor/MongoScheduledThreadPoolExecutor do it.

}

@Nullable
private static Throwable getException(final Runnable r, @Nullable final Throwable t) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VAKOTODO Call commonExecutor().uncaughtError only if the Throwable is in a Future. Otherwise, the Throwable will be delivered to the uncaught exception handler anyway, and our call will cause an extra handler invocation.

*
* @see #uncaughtError(Error)
*/
private final ExecutorService uncaughtExceptionHandlerExecutor;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VAKOTODO We don't need this. It is always OK to call Thread.currentThread().getUncaughtExceptionHandler().

Throwable exception = getException(r, t);
if (exception instanceof Error && t == null) {
// the `Error` is held in `r`, and would have not been thrown to be handled by the uncaught exception handler
commonExecutor().uncaughtError((Error) exception);

@stIncMale stIncMale Jul 22, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

VAKOTODO Instead of calling the uncaught exception handler explicitly, throw from afterExecute, and let the handler be called the way it is always called. Make sure this works out OK even for CommonExecutor.scheduler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant