Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -10,6 +10,8 @@
import javax.net.ssl.X509TrustManager;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
Expand Down Expand Up @@ -379,4 +381,52 @@ public DatabaseClientBuilder withTrustStoreAlgorithm(String algorithm) {
props.put(PREFIX + "ssl.truststore.algorithm", algorithm);
return this;
}

/**
* Sets the read timeout for HTTP requests made by the underlying OkHttp client.
*
* <p>By default, the read timeout is zero (no timeout), which is intentional for use cases
* that transfer large amounts of data (e.g., bulk exports or large document reads). Setting
* a non-zero timeout is recommended for applications that do not expect large responses, as
* an indefinitely blocked read thread can exhaust thread pools if the MarkLogic server
* becomes unresponsive.</p>
*
* @param value the timeout duration; use 0 to disable the timeout
* @param unit the time unit of the duration
* @return this builder
* @since 8.2.0
*/
public DatabaseClientBuilder withReadTimeout(long value, TimeUnit unit) {
Objects.requireNonNull(unit, "unit must not be null");
if (value < 0) {
throw new IllegalArgumentException(
"Timeout value must be zero (no timeout) or a positive duration, but was: " + value);
}
props.put(PREFIX + "readTimeoutMillis", unit.toMillis(value));
return this;
}

/**
* Sets the write timeout for HTTP requests made by the underlying OkHttp client.
*
* <p>By default, the write timeout is zero (no timeout), which is intentional for use cases
* that transfer large amounts of data (e.g., bulk imports or large document writes). Setting
* a non-zero timeout is recommended for applications that do not expect large request bodies,
* as an indefinitely blocked write thread can exhaust thread pools if the MarkLogic server
* becomes unresponsive.</p>
*
* @param value the timeout duration; use 0 to disable the timeout
* @param unit the time unit of the duration
* @return this builder
* @since 8.2.0
*/
public DatabaseClientBuilder withWriteTimeout(long value, TimeUnit unit) {
Objects.requireNonNull(unit, "unit must not be null");
if (value < 0) {
throw new IllegalArgumentException(
"Timeout value must be zero (no timeout) or a positive duration, but was: " + value);
}
props.put(PREFIX + "writeTimeoutMillis", unit.toMillis(value));
return this;
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -1265,21 +1265,70 @@ static public DatabaseClient newClient(String host, int port, String database,
static public DatabaseClient newClient(String host, int port, String basePath, String database,
SecurityContext securityContext,
DatabaseClient.ConnectionType connectionType) {
// As of 6.1.0, the following optimization is made as it's guaranteed that if the user is connecting to a
// Progress Data Cloud instance, then port 443 will be used. Every path for constructing a DatabaseClient goes through
// this method, ensuring that this optimization will always be applied, and thus freeing the user from having to
// worry about what port to configure when using Progress Data Cloud.
return newClient(host, port, basePath, database, securityContext, connectionType, 0L, 0L);
}

/**
* Creates a client with configurable read and write timeouts for HTTP requests.
*
* <p>As of 6.1.0, the Progress Data Cloud port optimization is applied here: port 443 is used automatically
* when connecting to a Progress Data Cloud instance.</p>
*
* @param host the MarkLogic host
* @param port the REST server port
* @param basePath optional base path
* @param database optional database name
* @param securityContext the security context
* @param connectionType whether the client connects directly or via a gateway
* @param readTimeoutMillis read timeout in milliseconds; 0 means no timeout
* @param writeTimeoutMillis write timeout in milliseconds; 0 means no timeout
* @return a new client for making database requests
* @since 8.2.0
*/
static public DatabaseClient newClient(String host, int port, String basePath, String database,
SecurityContext securityContext,
DatabaseClient.ConnectionType connectionType,
long readTimeoutMillis, long writeTimeoutMillis) {
// As of 6.1.0, port 443 is forced for Progress Data Cloud connections, freeing the caller
// from having to specify the port when using Progress Data Cloud.
if (securityContext instanceof ProgressDataCloudAuthContext) {
port = 443;
}

OkHttpServices.ConnectionConfig config = new OkHttpServices.ConnectionConfig(host, port, basePath, database, securityContext, clientConfigurators);
OkHttpServices.ConnectionConfig config = new OkHttpServices.ConnectionConfig(host, port, basePath, database, securityContext, clientConfigurators, readTimeoutMillis, writeTimeoutMillis);
RESTServices services = new OkHttpServices(config);
DatabaseClientImpl client = new DatabaseClientImpl(services, host, port, basePath, database, securityContext, connectionType);
client.setHandleRegistry(getHandleRegistry().copy());
return client;
}

/**
* Creates a client based on the given bean's properties, including any configured read and write timeouts.
* Unlike the other {@code newClient} overloads, this method uses the bean's own handle registry rather than
* the global {@link #getHandleRegistry()} registry.
*
* @param bean the bean describing the connection and timeout configuration
* @return a new client for making database requests
* @since 8.2.0
*/
static public DatabaseClient newClient(Bean bean) {
String host = bean.getHost();
int port = bean.getPort();
SecurityContext securityContext = bean.getSecurityContext();
// Port 443 is forced for Progress Data Cloud connections, freeing the caller
// from having to specify the port when using Progress Data Cloud.
if (securityContext instanceof ProgressDataCloudAuthContext) {
port = 443;
}
OkHttpServices.ConnectionConfig config = new OkHttpServices.ConnectionConfig(
host, port, bean.getBasePath(), bean.getDatabase(), securityContext, clientConfigurators,
bean.getReadTimeoutMillis(), bean.getWriteTimeoutMillis());
RESTServices services = new OkHttpServices(config);
DatabaseClientImpl client = new DatabaseClientImpl(services, host, port, bean.getBasePath(),
bean.getDatabase(), securityContext, bean.getConnectionType());
client.setHandleRegistry(bean.getHandleRegistry().copy());
return client;
}

/**
* Returns the default registry with factories for creating handles
* as adapters for IO representations. To create custom registries,
Expand Down Expand Up @@ -1350,6 +1399,8 @@ static public class Bean implements Serializable {
transient private SecurityContext securityContext;
transient private HandleFactoryRegistry handleRegistry =
HandleFactoryRegistryImpl.newDefault();
private long readTimeoutMillis = 0;
private long writeTimeoutMillis = 0;

/**
* Zero-argument constructor for bean applications. Other
Expand Down Expand Up @@ -1502,10 +1553,35 @@ public void registerDefaultHandles() {
* @return a new client for making database requests
*/
public DatabaseClient newClient() {
DatabaseClientImpl client = (DatabaseClientImpl) DatabaseClientFactory.newClient(
host, port, basePath, database, securityContext, connectionType);
client.setHandleRegistry(getHandleRegistry().copy());
return client;
return DatabaseClientFactory.newClient(this);
}

/**
* Returns the read timeout in milliseconds applied to HTTP requests. Zero means no timeout (the default).
* @return the read timeout in milliseconds
* @since 8.2.0
*/
public long getReadTimeoutMillis() { return readTimeoutMillis; }

/**
* Sets the read timeout for HTTP requests in milliseconds. Zero means no timeout (the default).
* @param readTimeoutMillis the read timeout in milliseconds
* @since 8.2.0
*/
public void setReadTimeoutMillis(long readTimeoutMillis) { this.readTimeoutMillis = readTimeoutMillis; }

/**
* Returns the write timeout in milliseconds applied to HTTP requests. Zero means no timeout (the default).
* @return the write timeout in milliseconds
* @since 8.2.0
*/
public long getWriteTimeoutMillis() { return writeTimeoutMillis; }

/**
* Sets the write timeout for HTTP requests in milliseconds. Zero means no timeout (the default).
* @param writeTimeoutMillis the write timeout in milliseconds
* @since 8.2.0
*/
public void setWriteTimeoutMillis(long writeTimeoutMillis) { this.writeTimeoutMillis = writeTimeoutMillis; }
}
}
Original file line number Diff line number Diff line change
@@ -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.impl;

Expand Down Expand Up @@ -105,6 +105,10 @@ public class DatabaseClientPropertySource {
DatabaseClientFactory.addConfigurator(new RemoveAcceptEncodingConfigurator());
}
});
connectionPropertyHandlers.put(PREFIX + "readTimeoutMillis", (bean, value) ->
bean.setReadTimeoutMillis(toLong(value)));
connectionPropertyHandlers.put(PREFIX + "writeTimeoutMillis", (bean, value) ->
bean.setWriteTimeoutMillis(toLong(value)));
}

public DatabaseClientPropertySource(Function<String, Object> propertySource) {
Expand All @@ -122,7 +126,7 @@ public DatabaseClient newClient() {
// DatabaseClientFactory.getHandleRegistry() will still impact the DatabaseClient returned by this method
// (and this behavior is expected by some existing tests).
return DatabaseClientFactory.newClient(bean.getHost(), bean.getPort(), bean.getBasePath(), bean.getDatabase(),
bean.getSecurityContext(), bean.getConnectionType());
bean.getSecurityContext(), bean.getConnectionType(), bean.getReadTimeoutMillis(), bean.getWriteTimeoutMillis());
}

/**
Expand Down Expand Up @@ -455,4 +459,17 @@ private SSLUtil.SSLInputs useNewSSLContext(String sslProtocol, X509TrustManager
}
return new SSLUtil.SSLInputs(sslContext, userTrustManager);
}

private static long toLong(Object value) {
long millis;
if (value instanceof Long) millis = (Long) value;
else if (value instanceof Integer) millis = ((Integer) value).longValue();
else if (value instanceof String) millis = Long.parseLong((String) value);
else throw new IllegalArgumentException("Timeout value must be a Long, Integer, or String; got: " + value.getClass());
if (millis < 0) {
throw new IllegalArgumentException(
"Timeout value must be zero (no timeout) or a positive duration, but was: " + millis);
}
return millis;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ static protected class ThreadState {
ThreadLocal.withInitial(() -> new ThreadState(useDigestAuthPing));

public record ConnectionConfig(String host, int port, String basePath, String database,
SecurityContext securityContext, List<OkHttpClientConfigurator> clientConfigurators) {
SecurityContext securityContext, List<OkHttpClientConfigurator> clientConfigurators,
long readTimeoutMillis, long writeTimeoutMillis) {
}
Comment on lines 153 to 156

public OkHttpServices(ConnectionConfig connectionConfig) {
Expand Down Expand Up @@ -220,6 +221,13 @@ private OkHttpClient connect(ConnectionConfig config) {

OkHttpClient.Builder clientBuilder = OkHttpUtil.newOkHttpClientBuilder(config.host, config.securityContext, config.clientConfigurators);

if (config.readTimeoutMillis() > 0) {
clientBuilder.readTimeout(config.readTimeoutMillis(), TimeUnit.MILLISECONDS);
}
if (config.writeTimeoutMillis() > 0) {
clientBuilder.writeTimeout(config.writeTimeoutMillis(), TimeUnit.MILLISECONDS);
}
Comment on lines +224 to +229

Properties props = System.getProperties();
if (props.containsKey(OKHTTP_LOGGINGINTERCEPTOR_LEVEL)) {
configureOkHttpLogging(clientBuilder, props);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.impl;

Expand Down Expand Up @@ -216,4 +216,34 @@ private DatabaseClientFactory.Bean buildBean() {
DatabaseClientPropertySource source = new DatabaseClientPropertySource(propertyName -> props.get(propertyName));
return source.newClientBean();
}

@Test
void readAndWriteTimeoutsFromPropertySource() {
props.put(PREFIX + "readTimeoutMillis", 5000L);
props.put(PREFIX + "writeTimeoutMillis", 10000L);
DatabaseClientFactory.Bean bean = buildBean();

assertEquals(5000L, bean.getReadTimeoutMillis());
assertEquals(10000L, bean.getWriteTimeoutMillis());
}

@Test
void readAndWriteTimeoutsFromPropertySourceAsStrings() {
props.put(PREFIX + "readTimeoutMillis", "15000");
props.put(PREFIX + "writeTimeoutMillis", "30000");
DatabaseClientFactory.Bean bean = buildBean();

assertEquals(15000L, bean.getReadTimeoutMillis());
assertEquals(30000L, bean.getWriteTimeoutMillis());
}

@Test
void readAndWriteTimeoutsFromPropertySourceAsIntegers() {
props.put(PREFIX + "readTimeoutMillis", 5000); // Integer, not Long
props.put(PREFIX + "writeTimeoutMillis", 10000);
DatabaseClientFactory.Bean bean = buildBean();

assertEquals(5000L, bean.getReadTimeoutMillis());
assertEquals(10000L, bean.getWriteTimeoutMillis());
}
}
Loading
Loading