From 3d2571b4aa4c909f317fa28dd3a245018970258d Mon Sep 17 00:00:00 2001 From: eric-zaharia Date: Thu, 4 Jun 2026 14:35:10 +0300 Subject: [PATCH 1/2] fix(java): ensure waitTask exception propagation --- .../src/main/java/com/algolia/search/TaskUtils.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/algoliasearch-core/src/main/java/com/algolia/search/TaskUtils.java b/algoliasearch-core/src/main/java/com/algolia/search/TaskUtils.java index 16f933eca..c3d5196f8 100644 --- a/algoliasearch-core/src/main/java/com/algolia/search/TaskUtils.java +++ b/algoliasearch-core/src/main/java/com/algolia/search/TaskUtils.java @@ -3,6 +3,7 @@ import com.algolia.search.exceptions.AlgoliaApiException; import com.algolia.search.exceptions.AlgoliaRetryException; import com.algolia.search.exceptions.AlgoliaRuntimeException; +import com.algolia.search.exceptions.LaunderThrowable; import com.algolia.search.models.RequestOptions; import com.algolia.search.models.common.TaskStatusResponse; import java.util.Objects; @@ -38,11 +39,13 @@ static void waitTask( try { response = getTaskAsync.apply(taskId, requestOptions).get(); - } catch (InterruptedException | ExecutionException e) { - // If the future was cancelled or the thread was interrupted or future completed - // exceptionally - // We stop - break; + } catch (InterruptedException e) { + // Restore the interrupted status and surface the failure + Thread.currentThread().interrupt(); + throw new AlgoliaRuntimeException(e); + } catch (ExecutionException e) { + // Surface the underlying error + throw LaunderThrowable.launder(e); } if (Objects.equals("published", response.getStatus())) return; From 43dc02580b4bb05d84e6a4d8f61d2a0922f6a47a Mon Sep 17 00:00:00 2001 From: eric-zaharia Date: Mon, 8 Jun 2026 10:32:00 +0300 Subject: [PATCH 2/2] fix(java): add tests --- .../com/algolia/search/TaskUtilsTest.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 algoliasearch-core/src/test/java/com/algolia/search/TaskUtilsTest.java diff --git a/algoliasearch-core/src/test/java/com/algolia/search/TaskUtilsTest.java b/algoliasearch-core/src/test/java/com/algolia/search/TaskUtilsTest.java new file mode 100644 index 000000000..55aacdb6d --- /dev/null +++ b/algoliasearch-core/src/test/java/com/algolia/search/TaskUtilsTest.java @@ -0,0 +1,71 @@ +package com.algolia.search; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.algolia.search.exceptions.AlgoliaApiException; +import com.algolia.search.exceptions.AlgoliaRuntimeException; +import com.algolia.search.models.RequestOptions; +import com.algolia.search.models.common.TaskStatusResponse; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class TaskUtilsTest { + + private static TaskStatusResponse status(String status) { + return new TaskStatusResponse().setStatus(status); + } + + @Test + @DisplayName("waitTask surfaces polling failures instead of silently exiting") + void propagatesExecutionException() { + AlgoliaApiException cause = new AlgoliaApiException("boom", 500); + BiFunction> failing = + (taskId, requestOptions) -> { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(cause); + return future; + }; + + // Before the fix this returned void (the exception was swallowed by a `break`), which let + // callers such as replaceAllObjects proceed as if the task had completed. It must now throw + // the unwrapped exception so the caller can abort. + assertThatThrownBy(() -> TaskUtils.waitTask(1L, 1L, null, failing)) + .isInstanceOf(AlgoliaApiException.class) + .isSameAs(cause); + } + + @Test + @DisplayName("waitTask restores the interrupt flag and throws when the thread is interrupted") + void restoresInterruptFlagOnInterruption() { + BiFunction> neverCompletes = + (taskId, requestOptions) -> new CompletableFuture<>(); + + Thread.currentThread().interrupt(); + try { + assertThatThrownBy(() -> TaskUtils.waitTask(1L, 1L, null, neverCompletes)) + .isInstanceOf(AlgoliaRuntimeException.class); + assertThat(Thread.currentThread().isInterrupted()).isTrue(); + } finally { + // Clear the flag so it does not leak into other tests. + Thread.interrupted(); + } + } + + @Test + @DisplayName("waitTask keeps polling until the task is published") + void retriesUntilPublished() { + AtomicInteger calls = new AtomicInteger(); + BiFunction> eventually = + (taskId, requestOptions) -> + CompletableFuture.completedFuture( + status(calls.incrementAndGet() < 3 ? "notPublished" : "published")); + + assertThatCode(() -> TaskUtils.waitTask(1L, 1L, null, eventually)).doesNotThrowAnyException(); + assertThat(calls.get()).isEqualTo(3); + } +}