From 91b722019d46d265ad6dde4c8d3a64b791902843 Mon Sep 17 00:00:00 2001 From: Vasiliy Mikhailov Date: Mon, 22 Jun 2026 21:18:56 +0300 Subject: [PATCH] Fix IllegalStateException in SchedulerToExecutorService.invokeAny invokeAny() guarded its result check with f.exceptionNow() == null, but Future.exceptionNow() throws IllegalStateException when the task completed normally (i.e. there is no exception), per its contract. As a result the first successfully completed task triggers an IllegalStateException instead of being returned. Both invokeAny overloads are affected. Replace the isDone/isCancelled/exceptionNow probe with a direct Future.State.SUCCESS check, which is the intended state query and does not throw. --- .../SchedulerToExecutorService.java | 4 +- .../SchedulerToExecutorServiceTest.java | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/test/java/io/reactivex/rxjava4/internal/schedulers/SchedulerToExecutorServiceTest.java diff --git a/src/main/java/io/reactivex/rxjava4/internal/schedulers/SchedulerToExecutorService.java b/src/main/java/io/reactivex/rxjava4/internal/schedulers/SchedulerToExecutorService.java index 729e0604fbd..5449901fab8 100644 --- a/src/main/java/io/reactivex/rxjava4/internal/schedulers/SchedulerToExecutorService.java +++ b/src/main/java/io/reactivex/rxjava4/internal/schedulers/SchedulerToExecutorService.java @@ -189,7 +189,7 @@ public T invokeAny(Collection> tasks) throws Interrupt while (!isTerminated()) { for (var f : result) { - if (f.isDone() && !f.isCancelled() && f.exceptionNow() == null) { + if (f.state() == Future.State.SUCCESS) { var v = f.resultNow(); @@ -224,7 +224,7 @@ public T invokeAny(Collection> tasks, long timeout, Ti totalTime--; for (var f : result) { - if (f.isDone() && !f.isCancelled() && f.exceptionNow() == null) { + if (f.state() == Future.State.SUCCESS) { var v = f.resultNow(); diff --git a/src/test/java/io/reactivex/rxjava4/internal/schedulers/SchedulerToExecutorServiceTest.java b/src/test/java/io/reactivex/rxjava4/internal/schedulers/SchedulerToExecutorServiceTest.java new file mode 100644 index 00000000000..a5493e1ffc5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava4/internal/schedulers/SchedulerToExecutorServiceTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava4.internal.schedulers; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava4.core.Scheduler; +import io.reactivex.rxjava4.disposables.Disposable; +import io.reactivex.rxjava4.schedulers.Schedulers; + +public class SchedulerToExecutorServiceTest { + + @Test + public void invokeAnyShouldReturnResultOfCompletedTask() throws Exception { + Scheduler scheduler = Schedulers.trampoline(); + SchedulerToExecutorService executor = new SchedulerToExecutorService( + scheduler, new AtomicReference<>(null)); + + Callable task1 = () -> "result1"; + Callable task2 = () -> "result2"; + + String result = executor.invokeAny(Arrays.asList(task1, task2)); + + assertNotNull("invokeAny should return a result", result); + assertTrue("result should be one of the task results", + result.equals("result1") || result.equals("result2")); + } + + @Test + public void invokeAnyWithSingleTask() throws Exception { + Scheduler scheduler = Schedulers.trampoline(); + SchedulerToExecutorService executor = new SchedulerToExecutorService( + scheduler, new AtomicReference<>(null)); + + Callable task = () -> 42; + + Integer result = executor.invokeAny(Arrays.asList(task)); + + assertEquals("invokeAny should return the single task result", Integer.valueOf(42), result); + } + + @Test + public void invokeAnyWithEmptyTasksShouldThrow() throws Exception { + Scheduler scheduler = Schedulers.trampoline(); + SchedulerToExecutorService executor = new SchedulerToExecutorService( + scheduler, new AtomicReference<>(null)); + + try { + executor.invokeAny(Arrays.asList()); + fail("invokeAny with empty tasks should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } + } +}