From a71cf12c1acd267b6520165012d8b603de7dd5c0 Mon Sep 17 00:00:00 2001 From: hwhang0917 <33582774+hwhang0917@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:10:36 +0900 Subject: [PATCH 1/2] feat(tasks): Return the finished Task from waitForTask and add Client timeout overload waitForTask returned void, so callers could not tell whether the task succeeded, failed or was canceled without a second getTask round trip. It now returns the Task in its final state, matching the other official Meilisearch SDKs. Client gains the (uid, timeoutInMs, intervalInMs) overload that Index already had, so the 5000ms default is no longer forced when waiting through Client. The timeout exception now carries the task uid, the timeout and the last observed status, and the loop returns as soon as a terminal status is seen instead of sleeping one more interval. Co-Authored-By: Claude Fable 5.1 --- src/main/java/com/meilisearch/sdk/Client.java | 23 +++++++- src/main/java/com/meilisearch/sdk/Index.java | 10 ++-- .../com/meilisearch/sdk/TasksHandler.java | 42 +++++++++------ .../meilisearch/integration/TasksTest.java | 53 ++++++++++++++++++- 4 files changed, 104 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/meilisearch/sdk/Client.java b/src/main/java/com/meilisearch/sdk/Client.java index d8d4cc686..17ce792ef 100644 --- a/src/main/java/com/meilisearch/sdk/Client.java +++ b/src/main/java/com/meilisearch/sdk/Client.java @@ -356,10 +356,29 @@ public TaskInfo deleteTasks(DeleteTasksQuery param) throws MeilisearchException * Waits for a task to be processed * * @param uid Identifier of the requested Task + * @return Task in its final state (succeeded, failed or canceled) * @throws MeilisearchException if an error occurs or if timeout is reached + * @see API + * specification */ - public void waitForTask(int uid) throws MeilisearchException { - this.tasksHandler.waitForTask(uid); + public Task waitForTask(int uid) throws MeilisearchException { + return this.tasksHandler.waitForTask(uid); + } + + /** + * Waits for a task to be processed + * + * @param uid Identifier of the requested Task + * @param timeoutInMs number of milliseconds before throwing an Exception + * @param intervalInMs number of milliseconds before requesting the status again + * @return Task in its final state (succeeded, failed or canceled) + * @throws MeilisearchException if an error occurs or if timeout is reached + * @see API + * specification + */ + public Task waitForTask(int uid, int timeoutInMs, int intervalInMs) + throws MeilisearchException { + return this.tasksHandler.waitForTask(uid, timeoutInMs, intervalInMs); } /** diff --git a/src/main/java/com/meilisearch/sdk/Index.java b/src/main/java/com/meilisearch/sdk/Index.java index 97d397b70..1cac80d7e 100644 --- a/src/main/java/com/meilisearch/sdk/Index.java +++ b/src/main/java/com/meilisearch/sdk/Index.java @@ -1307,12 +1307,13 @@ public TasksResults getTasks(TasksQuery param) throws MeilisearchException { * Waits for a task to be processed * * @param taskId Identifier of the requested Task + * @return Task in its final state (succeeded, failed or canceled) * @throws MeilisearchException if an error occurs or if timeout is reached * @see API * specification */ - public void waitForTask(int taskId) throws MeilisearchException { - this.tasksHandler.waitForTask(taskId, 5000, 50); + public Task waitForTask(int taskId) throws MeilisearchException { + return this.tasksHandler.waitForTask(taskId); } /** @@ -1321,13 +1322,14 @@ public void waitForTask(int taskId) throws MeilisearchException { * @param taskId ID of the index update * @param timeoutInMs number of milliseconds before throwing an Exception * @param intervalInMs number of milliseconds before requesting the status again + * @return Task in its final state (succeeded, failed or canceled) * @throws MeilisearchException if an error occurs or if timeout is reached * @see API * specification */ - public void waitForTask(int taskId, int timeoutInMs, int intervalInMs) + public Task waitForTask(int taskId, int timeoutInMs, int intervalInMs) throws MeilisearchException { - this.tasksHandler.waitForTask(taskId, timeoutInMs, intervalInMs); + return this.tasksHandler.waitForTask(taskId, timeoutInMs, intervalInMs); } /** diff --git a/src/main/java/com/meilisearch/sdk/TasksHandler.java b/src/main/java/com/meilisearch/sdk/TasksHandler.java index 3692d9192..da2ec0e04 100644 --- a/src/main/java/com/meilisearch/sdk/TasksHandler.java +++ b/src/main/java/com/meilisearch/sdk/TasksHandler.java @@ -6,7 +6,6 @@ import com.meilisearch.sdk.model.*; import com.meilisearch.sdk.model.batch.req.BatchesQuery; import com.meilisearch.sdk.model.batch.res.Batch; -import java.util.Date; /** * Class covering the Meilisearch Task API @@ -14,6 +13,9 @@ * @see API specification */ public class TasksHandler { + static final int DEFAULT_WAIT_TIMEOUT_MS = 5000; + static final int DEFAULT_WAIT_INTERVAL_MS = 50; + private final HttpClient httpClient; /** @@ -124,10 +126,11 @@ TaskInfo deleteTasks(DeleteTasksQuery param) throws MeilisearchException { * Waits for a task to be processed * * @param taskUid Identifier of the Task + * @return Task in its final state (succeeded, failed or canceled) * @throws MeilisearchException if timeout is reached */ - void waitForTask(int taskUid) throws MeilisearchException { - this.waitForTask(taskUid, 5000, 50); + Task waitForTask(int taskUid) throws MeilisearchException { + return this.waitForTask(taskUid, DEFAULT_WAIT_TIMEOUT_MS, DEFAULT_WAIT_INTERVAL_MS); } /** @@ -136,28 +139,33 @@ void waitForTask(int taskUid) throws MeilisearchException { * @param taskUid Identifier of the Task * @param timeoutInMs number of milliseconds before throwing an Exception * @param intervalInMs number of milliseconds before requesting the status again + * @return Task in its final state (succeeded, failed or canceled) * @throws MeilisearchException if timeout is reached */ - void waitForTask(int taskUid, int timeoutInMs, int intervalInMs) throws MeilisearchException { - Task task; - TaskStatus status = null; - long startTime = new Date().getTime(); - long elapsedTime = 0; - - while (status == null - || (status.equals(TaskStatus.ENQUEUED) || status.equals(TaskStatus.PROCESSING))) { - if (elapsedTime >= timeoutInMs) { - throw new MeilisearchTimeoutException(); + Task waitForTask(int taskUid, int timeoutInMs, int intervalInMs) throws MeilisearchException { + long deadline = System.currentTimeMillis() + timeoutInMs; + while (true) { + Task task = this.getTask(taskUid); + TaskStatus status = task.getStatus(); + if (status != TaskStatus.ENQUEUED && status != TaskStatus.PROCESSING) { + return task; + } + if (System.currentTimeMillis() >= deadline) { + throw new MeilisearchTimeoutException( + "Task " + + taskUid + + " not finished after " + + timeoutInMs + + "ms (last status: " + + status + + ")"); } - task = this.getTask(taskUid); - status = task.getStatus(); try { Thread.sleep(intervalInMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new MeilisearchTimeoutException(); + throw new MeilisearchTimeoutException(e); } - elapsedTime = new Date().getTime() - startTime; } } diff --git a/src/test/java/com/meilisearch/integration/TasksTest.java b/src/test/java/com/meilisearch/integration/TasksTest.java index 391b57ca4..b8d096b95 100644 --- a/src/test/java/com/meilisearch/integration/TasksTest.java +++ b/src/test/java/com/meilisearch/integration/TasksTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.blankOrNullString; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.instanceOf; @@ -14,6 +15,7 @@ import com.meilisearch.integration.classes.AbstractIT; import com.meilisearch.integration.classes.TestData; import com.meilisearch.sdk.Index; +import com.meilisearch.sdk.exceptions.MeilisearchTimeoutException; import com.meilisearch.sdk.model.*; import com.meilisearch.sdk.utils.Movie; import java.time.Instant; @@ -340,9 +342,58 @@ public void testWaitForTaskTimoutInMs() throws Exception { Index index = client.index(indexUid); TaskInfo task = index.addDocuments(this.testData.getRaw()); + + MeilisearchTimeoutException e = + assertThrows( + MeilisearchTimeoutException.class, + () -> index.waitForTask(task.getTaskUid(), 0, 50)); + assertThat(e.getMessage(), containsString("Task " + task.getTaskUid())); + assertThat(e.getMessage(), containsString("0ms")); + index.waitForTask(task.getTaskUid()); + } + + /** Test waitForTask returns the finished task */ + @Test + public void testWaitForTaskReturnsTask() throws Exception { + String indexUid = "WaitForTaskReturnsTask"; + TaskInfo response = client.createIndex(indexUid); + + Task task = client.waitForTask(response.getTaskUid()); + + assertThat(task.getUid(), is(equalTo(response.getTaskUid()))); + assertThat(task.getStatus(), is(equalTo(TaskStatus.SUCCEEDED))); + assertThat(task.getFinishedAt(), is(notNullValue())); + + client.deleteIndex(indexUid); + } + + /** Test waitForTask returns a failed task instead of throwing */ + @Test + public void testWaitForTaskReturnsFailedTask() throws Exception { + String indexUid = "WaitForTaskReturnsFailedTask"; + client.waitForTask(client.createIndex(indexUid).getTaskUid()); + + TaskInfo response = client.createIndex(indexUid); + Task task = client.waitForTask(response.getTaskUid()); + + assertThat(task.getStatus(), is(equalTo(TaskStatus.FAILED))); + assertThat(task.getError().getCode(), is(equalTo("index_already_exists"))); + + client.deleteIndex(indexUid); + } + + /** Test Client.waitForTask with timeoutInMs and intervalInMs */ + @Test + public void testClientWaitForTaskTimeoutInMs() throws Exception { + String indexUid = "ClientWaitForTaskTimeoutInMs"; + TaskInfo response = client.createIndex(indexUid); + + Task task = client.waitForTask(response.getTaskUid(), 10000, 50); + + assertThat(task.getStatus(), is(equalTo(TaskStatus.SUCCEEDED))); - assertThrows(Exception.class, () -> index.waitForTask(task.getTaskUid(), 0, 50)); + client.deleteIndex(indexUid); } /** Test Tasks with Jackson Json Handler */ From da0519344d3dd82eed20c366a14e6288fcfea4eb Mon Sep 17 00:00:00 2001 From: hwhang0917 <33582774+hwhang0917@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:31:14 +0900 Subject: [PATCH 2/2] fix(tasks): Never sleep past the waitForTask deadline When intervalInMs is larger than the remaining timeout, the loop slept for the full interval and could block far longer than timeoutInMs. Clamp each sleep to the time left so the call returns or times out within one poll of the deadline. Co-Authored-By: Claude Fable 5.1 --- .../com/meilisearch/sdk/TasksHandler.java | 6 ++++-- .../meilisearch/integration/TasksTest.java | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/meilisearch/sdk/TasksHandler.java b/src/main/java/com/meilisearch/sdk/TasksHandler.java index da2ec0e04..47756c252 100644 --- a/src/main/java/com/meilisearch/sdk/TasksHandler.java +++ b/src/main/java/com/meilisearch/sdk/TasksHandler.java @@ -150,7 +150,8 @@ Task waitForTask(int taskUid, int timeoutInMs, int intervalInMs) throws Meilisea if (status != TaskStatus.ENQUEUED && status != TaskStatus.PROCESSING) { return task; } - if (System.currentTimeMillis() >= deadline) { + long remainingMs = deadline - System.currentTimeMillis(); + if (remainingMs <= 0) { throw new MeilisearchTimeoutException( "Task " + taskUid @@ -161,7 +162,8 @@ Task waitForTask(int taskUid, int timeoutInMs, int intervalInMs) throws Meilisea + ")"); } try { - Thread.sleep(intervalInMs); + // never sleep past the deadline, even when intervalInMs exceeds timeoutInMs + Thread.sleep(Math.min(intervalInMs, remainingMs)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new MeilisearchTimeoutException(e); diff --git a/src/test/java/com/meilisearch/integration/TasksTest.java b/src/test/java/com/meilisearch/integration/TasksTest.java index b8d096b95..8c86627b9 100644 --- a/src/test/java/com/meilisearch/integration/TasksTest.java +++ b/src/test/java/com/meilisearch/integration/TasksTest.java @@ -7,6 +7,7 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -353,6 +354,25 @@ public void testWaitForTaskTimoutInMs() throws Exception { index.waitForTask(task.getTaskUid()); } + /** Test waitForTask does not sleep past the timeout when intervalInMs exceeds it */ + @Test + public void testWaitForTaskIntervalLongerThanTimeout() throws Exception { + String indexUid = "WaitForTaskIntervalLongerThanTimeout"; + Index index = client.index(indexUid); + TaskInfo task = index.addDocuments(this.testData.getRaw()); + + long start = System.currentTimeMillis(); + try { + index.waitForTask(task.getTaskUid(), 100, 10000); + } catch (MeilisearchTimeoutException ignored) { + // either outcome is fine, only the elapsed time matters + } + + assertThat(System.currentTimeMillis() - start, is(lessThan(5000L))); + + index.waitForTask(task.getTaskUid()); + } + /** Test waitForTask returns the finished task */ @Test public void testWaitForTaskReturnsTask() throws Exception {