Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/bin
bin
.classpath
build
.gradle
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
version=6.1-SNAPSHOT
version=6.2-SNAPSHOT

# See https://github.com/FasterXML/jackson for more information the Jackson libraries.
# This should match the version used by the MarkLogic Java Client.
jacksonVersion=2.20.0

springVersion=6.2.11
springVersion=6.2.12

# Define these on the command line to publish to OSSRH
# See https://central.sonatype.org/publish/publish-gradle/#credentials for more information
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.marklogic.appdeployer.util.SimplePropertiesSource
import com.marklogic.gradle.task.*
import com.marklogic.gradle.task.admin.InitTask
import com.marklogic.gradle.task.admin.InstallAdminTask
import com.marklogic.gradle.task.admin.WaitTillReadyTask
import com.marklogic.gradle.task.alert.DeleteAllAlertConfigsTask
import com.marklogic.gradle.task.alert.DeployAlertingTask
import com.marklogic.gradle.task.client.*
Expand Down Expand Up @@ -125,6 +126,7 @@ class MarkLogicPlugin implements Plugin<Project> {
project.task("mlInit", type: InitTask, group: adminGroup, description: "Perform a one-time initialization of a MarkLogic server; uses the properties 'mlLicenseKey' and 'mlLicensee'")
project.task("mlInstallAdmin", type: InstallAdminTask, group: adminGroup, description: "Perform a one-time installation of an admin user; uses the properties 'mlUsername' and 'mlPassword'; " +
"the realm, which defaults to 'public', can optionally be specified on the command line via '-Prealm='")
project.task("mlWaitTillReady", type: WaitTillReadyTask, group: adminGroup, description: "Wait until MarkLogic is ready and accessible; useful in CI/CD pipelines after installing or restarting MarkLogic.")

String alertGroup = "ml-gradle Alert"
project.task("mlDeleteAllAlertConfigs", type: DeleteAllAlertConfigsTask, group: alertGroup, description: "Delete all alert configs, which also deletes all of the actions rules associated with them")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.gradle.task.admin


import com.marklogic.client.ext.util.ConnectionChecker
import com.marklogic.gradle.task.MarkLogicTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction

/**
* Waits until MarkLogic is ready by using ConnectionChecker to verify database connectivity via the App-Services port (8000).
* This is useful in CI/CD pipelines where MarkLogic may have just been installed or restarted.
* Delegates to ConnectionChecker for the actual connection testing logic.
*/
class WaitTillReadyTask extends MarkLogicTask {

@Input
@Optional
Long waitInterval = 3000L

@Input
@Optional
Integer maxAttempts = 20

@TaskAction
void waitTillReady() {
new ConnectionChecker(
{ -> getAppConfig().newAppServicesDatabaseClient(null) } as java.util.function.Supplier,
waitInterval,
maxAttempts
).waitUntilReady()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.ext.util;

import com.marklogic.client.DatabaseClient;
import com.marklogic.client.ext.helper.LoggingObject;

import java.util.function.Supplier;

/**
* Immutable utility class for checking if a MarkLogic DatabaseClient connection is ready and functional.
* This is particularly useful in CI/CD pipelines where MarkLogic may have just been installed
* or restarted, and you need to wait until it's ready to handle database requests.
*/

public class ConnectionChecker extends LoggingObject {

private final Supplier<DatabaseClient> clientSupplier;
private final long waitInterval; // milliseconds
private final int maxAttempts;

/**
* Create a new ConnectionChecker with the given DatabaseClient supplier and default settings.
* Default wait interval is 3000ms, default max attempts is 20.
*
* @param clientSupplier a Supplier that creates a new DatabaseClient for each attempt
*/
public ConnectionChecker(Supplier<DatabaseClient> clientSupplier) {
this(clientSupplier, 3000L, 20);
}

/**
* Create a new ConnectionChecker with the given DatabaseClient supplier and custom settings.
*
* @param clientSupplier a Supplier that creates a new DatabaseClient for each attempt
* @param waitIntervalMs wait interval in milliseconds (must be positive)
* @param maxAttempts maximum number of attempts (must be positive)
*/
public ConnectionChecker(Supplier<DatabaseClient> clientSupplier, long waitIntervalMs, int maxAttempts) {
if (clientSupplier == null) {
throw new IllegalArgumentException("DatabaseClient supplier cannot be null");
}
if (waitIntervalMs <= 0) {
throw new IllegalArgumentException("Wait interval must be positive");
}
if (maxAttempts <= 0) {
throw new IllegalArgumentException("Max attempts must be positive");
}
this.clientSupplier = clientSupplier;
this.waitInterval = waitIntervalMs;
this.maxAttempts = maxAttempts;
}

/**
* Wait until the DatabaseClient connection is ready, retrying up to maxAttempts times.
*
* @return true when the connection becomes ready
* @throws RuntimeException if the connection is not ready after maxAttempts
*/
public boolean waitUntilReady() {
logger.info("Waiting for MarkLogic to be ready (checking every {}ms, max attempts: {})", waitInterval, maxAttempts);

for (int attempt = 1; attempt <= maxAttempts; attempt++) {
String errorMessage;
try (DatabaseClient client = clientSupplier.get()) {
DatabaseClient.ConnectionResult result = client.checkConnection();
if (result.isConnected()) {
logger.info("MarkLogic is ready (connected on attempt {})", attempt);
return true;
}
errorMessage = "Attempt %d failed with status code %d: %s".formatted(attempt, result.getStatusCode(), result.getErrorMessage());
} catch (Exception e) {
errorMessage = "Attempt %d failed: %s".formatted(attempt, e.getMessage());
}

if (attempt < maxAttempts) {
logger.info("{}; waiting {}ms before retry...", errorMessage, waitInterval);
try {
Thread.sleep(waitInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Connection check was interrupted", e);
}
}
}

throw new RuntimeException(format("Failed to connect to MarkLogic after %d attempts (waited %dms between attempts)", maxAttempts, waitInterval));
}

public long getWaitInterval() {
return waitInterval;
}

public int getMaxAttempts() {
return maxAttempts;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.ext.util;

import com.marklogic.client.DatabaseClient;
import com.marklogic.client.FailedRequestException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class ConnectionCheckerTest {

@Mock
private DatabaseClient mockClient;

@Mock
private DatabaseClient.ConnectionResult mockConnectionResult;

private ConnectionChecker connectionChecker;

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
connectionChecker = new ConnectionChecker(() -> mockClient);
}

@Test
void testConstructorWithNullClient() {
assertThrows(IllegalArgumentException.class, () -> new ConnectionChecker((java.util.function.Supplier<DatabaseClient>) null));
assertThrows(IllegalArgumentException.class, () -> new ConnectionChecker((java.util.function.Supplier<DatabaseClient>) null, 2000L, 15));
}

@Test
void testConstructorWithCustomSettings() {
ConnectionChecker checker = new ConnectionChecker(() -> mockClient, 5000L, 30);
assertEquals(5000L, checker.getWaitInterval());
assertEquals(30, checker.getMaxAttempts());
}

@Test
void testConstructorWithInvalidSettings() {
assertThrows(IllegalArgumentException.class, () -> new ConnectionChecker(() -> mockClient, 0L, 15));
assertThrows(IllegalArgumentException.class, () -> new ConnectionChecker(() -> mockClient, -1L, 15));
assertThrows(IllegalArgumentException.class, () -> new ConnectionChecker(() -> mockClient, 2000L, 0));
assertThrows(IllegalArgumentException.class, () -> new ConnectionChecker(() -> mockClient, 2000L, -1));
}

@Test
void testWaitUntilReadySuccessFirstTry() {
when(mockClient.checkConnection()).thenReturn(mockConnectionResult);
when(mockConnectionResult.isConnected()).thenReturn(true);

assertTrue(connectionChecker.waitUntilReady());
verify(mockClient, times(1)).checkConnection();
}

@Test
void testWaitUntilReadySuccessAfterRetries() {
ConnectionChecker checker = new ConnectionChecker(() -> mockClient, 100L, 3);

when(mockClient.checkConnection()).thenReturn(mockConnectionResult);
when(mockConnectionResult.isConnected())
.thenReturn(false) // First attempt fails
.thenReturn(false) // Second attempt fails
.thenReturn(true); // Third attempt succeeds

assertTrue(checker.waitUntilReady());
verify(mockClient, times(3)).checkConnection();
}

@Test
void testWaitUntilReadyFailsAfterMaxAttempts() {
ConnectionChecker checker = new ConnectionChecker(() -> mockClient, 100L, 2);

when(mockClient.checkConnection()).thenThrow(new FailedRequestException("Connection failed"));

RuntimeException exception = assertThrows(RuntimeException.class,
() -> checker.waitUntilReady());

assertTrue(exception.getMessage().contains("Failed to connect to MarkLogic after 2 attempts"));
verify(mockClient, times(2)).checkConnection();
}

@Test
void testDefaultValues() {
assertEquals(3000L, connectionChecker.getWaitInterval());
assertEquals(20, connectionChecker.getMaxAttempts());
}

@Test
void testImmutability() {
// Test that objects are immutable - getters return the values set in constructor
ConnectionChecker checker1 = new ConnectionChecker(() -> mockClient);
assertEquals(3000L, checker1.getWaitInterval());
assertEquals(20, checker1.getMaxAttempts());

ConnectionChecker checker2 = new ConnectionChecker(() -> mockClient, 5000L, 30);
assertEquals(5000L, checker2.getWaitInterval());
assertEquals(30, checker2.getMaxAttempts());

// Original checker should still have default values
assertEquals(3000L, checker1.getWaitInterval());
assertEquals(20, checker1.getMaxAttempts());
}
}