diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/datamovement/impl/DataMovementServices.java b/marklogic-client-api/src/main/java/com/marklogic/client/datamovement/impl/DataMovementServices.java index eb58c15e2..de1754a50 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/datamovement/impl/DataMovementServices.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/datamovement/impl/DataMovementServices.java @@ -71,6 +71,13 @@ QueryConfig initConfig(String method, SearchQueryDefinition qdef) { logger.debug("initialized maxDocToUriBatchRatio to : {}", maxDocToUriBatchRatio); } + // Per GitHub bug 1872 and MLE-26460, the server may return -1 when there are fewer server threads than forests. + // A value of -1 will cause later problems when constructing a LinkedBlockingQueue with a negative capacity. + // So defaulting this to 1 to avoid later errors. + if (maxDocToUriBatchRatio <= 0) { + maxDocToUriBatchRatio = 1; + } + JsonNode defaultDocBatchSizeNode = result.get("defaultDocBatchSize"); int defaultDocBatchSize = -1; if (defaultDocBatchSizeNode != null && defaultDocBatchSizeNode.isInt()) { diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/datamovement/FewerServerThreadsThanForestsTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/datamovement/FewerServerThreadsThanForestsTest.java new file mode 100644 index 000000000..b7d7b1650 --- /dev/null +++ b/marklogic-client-api/src/test/java/com/marklogic/client/datamovement/FewerServerThreadsThanForestsTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + */ +package com.marklogic.client.datamovement; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.marklogic.client.DatabaseClient; +import com.marklogic.client.test.AbstractClientTest; +import com.marklogic.client.test.Common; +import com.marklogic.mgmt.ManageClient; +import com.marklogic.mgmt.resource.appservers.ServerManager; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FewerServerThreadsThanForestsTest extends AbstractClientTest { + + @Test + void test() { + DatabaseClient client = Common.newClient(); + final int forestCount = client.newDataMovementManager().readForestConfig().listForests().length; + if (forestCount < 2) { + logger.info("This test requires multiple forests so that the server thread count can be set to the " + + "number of forests minus one; skipping test"); + return; + } + + adjustServerThreads(forestCount - 1); + try { + DataMovementManager dmm = client.newDataMovementManager(); + AtomicInteger uriCount = new AtomicInteger(); + QueryBatcher queryBatcher = dmm.newQueryBatcher(client.newQueryManager().newStructuredQueryBuilder().collection("/optic/test")) + .withThreadCount(1) + .onUrisReady(batch -> uriCount.addAndGet(batch.getItems().length)); + dmm.startJob(queryBatcher); + queryBatcher.awaitCompletion(); + dmm.stopJob(queryBatcher); + + assertEquals(4, uriCount.get(), "Verifies that the 4 test documents were found, and more importantly, " + + "that the new default maxDocToUriBatchRatio of 1 was applied correctly when the number of " + + "server threads is less than the number of forests. This is for bug 1872 in GitHub. Prior to this " + + "fix, the maxDocToUriBatchRatio of -1 returned by the server caused an error when the " + + "LinkedBlockingQueue was constructed with a negative capacity."); + } finally { + // We can safely use this number because we know the test-app doesn't change this. + final int defaultServerThreadCount = 32; + adjustServerThreads(defaultServerThreadCount); + } + } + + private void adjustServerThreads(final int threads) { + logger.info("Adjusting server threads to {}", threads); + Common.newAdminManager().invokeActionRequiringRestart(() -> { + ManageClient manageClient = Common.newManageClient(); + ObjectNode payload = Common.newServerPayload().put("threads", threads); + new ServerManager(manageClient).save(payload.toString()); + return true; + }); + } +} diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java index 2437bf255..b5a45af95 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. + * Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ package com.marklogic.client.test; @@ -13,6 +13,8 @@ import com.marklogic.client.io.DocumentMetadataHandle; import com.marklogic.mgmt.ManageClient; import com.marklogic.mgmt.ManageConfig; +import com.marklogic.mgmt.admin.AdminConfig; +import com.marklogic.mgmt.admin.AdminManager; import org.springframework.util.FileCopyUtils; import org.w3c.dom.DOMException; import org.w3c.dom.Document; @@ -258,6 +260,10 @@ public static ManageClient newManageClient() { return new ManageClient(new ManageConfig(HOST, 8002, SERVER_ADMIN_USER, SERVER_ADMIN_PASS)); } + public static AdminManager newAdminManager() { + return new AdminManager(new AdminConfig(HOST, 8001, SERVER_ADMIN_USER, SERVER_ADMIN_PASS)); + } + public static ObjectNode newServerPayload() { ObjectNode payload = new ObjectMapper().createObjectNode(); payload.put("server-name", SERVER_NAME);