Skip to content

Add transaction timeout and metadata options to QueryConfig #1506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 5, 2023
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
88 changes: 85 additions & 3 deletions driver/src/main/java/org/neo4j/driver/QueryConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@
package org.neo4j.driver;

import static java.util.Objects.requireNonNull;
import static org.neo4j.driver.internal.util.Preconditions.checkArgument;

import java.io.Serial;
import java.io.Serializable;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.neo4j.driver.internal.util.Extract;

/**
* Query configuration used by {@link Driver#executableQuery(String)} and its variants.
Expand Down Expand Up @@ -55,6 +60,16 @@ public final class QueryConfig implements Serializable {
* The flag indicating if default bookmark manager should be used.
*/
private final boolean useDefaultBookmarkManager;
/**
* The transaction timeout.
* @since 5.16
*/
private final Duration timeout;
/**
* The transaction metadata.
* @since 5.16
*/
private final Map<String, Serializable> metadata;

/**
* Returns default config value.
Expand All @@ -71,6 +86,8 @@ private QueryConfig(Builder builder) {
this.impersonatedUser = builder.impersonatedUser;
this.bookmarkManager = builder.bookmarkManager;
this.useDefaultBookmarkManager = builder.useDefaultBookmarkManager;
this.timeout = builder.timeout;
this.metadata = builder.metadata;
}

/**
Expand Down Expand Up @@ -121,6 +138,26 @@ public Optional<BookmarkManager> bookmarkManager(BookmarkManager defaultBookmark
return useDefaultBookmarkManager ? Optional.of(defaultBookmarkManager) : Optional.ofNullable(bookmarkManager);
}

/**
* Get the configured transaction timeout.
*
* @return an {@link Optional} containing the configured timeout or {@link Optional#empty()} otherwise
* @since 5.16
*/
public Optional<Duration> timeout() {
return Optional.ofNullable(timeout);
}

/**
* Get the configured transaction metadata.
*
* @return metadata or empty map when it is not configured
* @since 5.16
*/
public Map<String, Serializable> metadata() {
return metadata;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -130,12 +167,15 @@ public boolean equals(Object o) {
&& routing == that.routing
&& Objects.equals(database, that.database)
&& Objects.equals(impersonatedUser, that.impersonatedUser)
&& Objects.equals(bookmarkManager, that.bookmarkManager);
&& Objects.equals(bookmarkManager, that.bookmarkManager)
&& Objects.equals(timeout, that.timeout)
&& Objects.equals(metadata, that.metadata);
}

@Override
public int hashCode() {
return Objects.hash(routing, database, impersonatedUser, bookmarkManager, useDefaultBookmarkManager);
return Objects.hash(
routing, database, impersonatedUser, bookmarkManager, useDefaultBookmarkManager, timeout, metadata);
}

@Override
Expand All @@ -145,7 +185,9 @@ public String toString() {
+ database + '\'' + ", impersonatedUser='"
+ impersonatedUser + '\'' + ", bookmarkManager="
+ bookmarkManager + ", useDefaultBookmarkManager="
+ useDefaultBookmarkManager + '}';
+ useDefaultBookmarkManager + '\'' + ", timeout='"
+ timeout + '\'' + ", metadata="
+ metadata + '}';
}

/**
Expand All @@ -157,6 +199,8 @@ public static final class Builder {
private String impersonatedUser;
private BookmarkManager bookmarkManager;
private boolean useDefaultBookmarkManager = true;
private Duration timeout;
private Map<String, Serializable> metadata = Collections.emptyMap();

private Builder() {}

Expand Down Expand Up @@ -216,6 +260,44 @@ public Builder withBookmarkManager(BookmarkManager bookmarkManager) {
return this;
}

/**
* Set the transaction timeout. Transactions that execute longer than the configured timeout will be terminated by the database.
* <p>
* This functionality allows user code to limit query/transaction execution time.
* The specified timeout overrides the default timeout configured in the database using the {@code db.transaction.timeout} setting ({@code dbms.transaction.timeout} before Neo4j 5.0).
* Values higher than {@code db.transaction.timeout} will be ignored and will fall back to the default for server versions between 4.2 and 5.2 (inclusive).
* <p>
* The provided value should not represent a negative duration.
* {@link Duration#ZERO} will make the transaction execute indefinitely.
*
* @param timeout the timeout.
* @return this builder.
* @since 5.16
*/
public Builder withTimeout(Duration timeout) {
if (timeout != null) {
checkArgument(!timeout.isNegative(), "Transaction timeout should not be negative");
}

this.timeout = timeout;
return this;
}

/**
* Set the transaction metadata.
*
* @param metadata the metadata, must not be {@code null}.
* @return this builder.
* @since 5.16
*/
public Builder withMetadata(Map<String, Serializable> metadata) {
requireNonNull(metadata, "Metadata should not be null");
metadata.values()
.forEach(Extract::assertParameter); // Just assert valid parameters but don't create a value map yet
this.metadata = Map.copyOf(metadata); // Create a defensive copy
return this;
}

/**
* Create a config instance from this builder.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ public Builder withDefaultTimeout() {
* @param metadata the metadata.
* @return this builder.
*/
public Builder withMetadata(Map<String, Object> metadata) {
public Builder withMetadata(Map<String, ?> metadata) {
requireNonNull(metadata, "Transaction metadata should not be null");
metadata.values()
.forEach(Extract::assertParameter); // Just assert valid parameters but don't create a value map yet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ public <A, R, T> T execute(Collector<Record, A, R> recordCollector, ResultFinish
return resultFinisher.finish(result.keys(), finishedValue, summary);
};
var accessMode = config.routing().equals(RoutingControl.WRITE) ? AccessMode.WRITE : AccessMode.READ;
var transactionConfigBuilder = TransactionConfig.builder();
config.timeout().ifPresent(transactionConfigBuilder::withTimeout);
transactionConfigBuilder.withMetadata(config.metadata());
return session.execute(
accessMode, txCallback, TransactionConfig.empty(), TelemetryApi.EXECUTABLE_QUERY, false);
accessMode, txCallback, transactionConfigBuilder.build(), TelemetryApi.EXECUTABLE_QUERY, false);
}
}

Expand Down
42 changes: 40 additions & 2 deletions driver/src/test/java/org/neo4j/driver/QueryConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;

import java.io.Serializable;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
Expand Down Expand Up @@ -56,7 +61,6 @@ void shouldUpdateRouting(RoutingControl routing) {
}

@Test
@SuppressWarnings("WriteOnlyObject")
void shouldNotAllowNullRouting() {
assertThrows(NullPointerException.class, () -> QueryConfig.builder().withRouting(null));
}
Expand Down Expand Up @@ -102,9 +106,41 @@ void shouldAllowNullBookmarkManager() {
assertTrue(config.bookmarkManager(mock(BookmarkManager.class)).isEmpty());
}

@Test
void shouldHaveEmptyMetadataByDefault() {
assertEquals(Collections.emptyMap(), QueryConfig.defaultConfig().metadata());
}

@Test
void shouldUpdateMetadata() {
var metadata = Map.<String, Serializable>of("k1", "v1", "k2", 0);
var config = QueryConfig.builder().withMetadata(metadata).build();

assertEquals(metadata, config.metadata());
}

@Test
void shouldHaveNullTimeoutByDefault() {
assertTrue(QueryConfig.defaultConfig().timeout().isEmpty());
}

@ParameterizedTest
@MethodSource("timeoutDurations")
void shouldUpdateTimeout(Duration timeout) {
var config = QueryConfig.builder().withTimeout(timeout).build();
assertEquals(timeout, config.timeout().orElse(null));
}

static Stream<Duration> timeoutDurations() {
return Stream.of(null, Duration.ZERO, Duration.ofMillis(5));
}

@Test
void shouldSerialize() throws Exception {
var originalConfig = QueryConfig.defaultConfig();
var originalConfig = QueryConfig.builder()
.withTimeout(Duration.ofSeconds(1))
.withMetadata(Map.of("k1", "v1", "k2", 1))
.build();
var deserializedConfig = TestUtil.serializeAndReadBack(originalConfig, QueryConfig.class);
var defaultManager = mock(BookmarkManager.class);

Expand All @@ -113,6 +149,8 @@ void shouldSerialize() throws Exception {
assertEquals(originalConfig.impersonatedUser(), deserializedConfig.impersonatedUser());
assertEquals(
originalConfig.bookmarkManager(defaultManager), deserializedConfig.bookmarkManager(defaultManager));
assertEquals(originalConfig.timeout(), deserializedConfig.timeout());
assertEquals(originalConfig.metadata(), deserializedConfig.metadata());
}

record ResultWithSummary<T>(T value, ResultSummary summary) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
package neo4j.org.testkit.backend.messages.requests;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.io.Serializable;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletionStage;
import lombok.Getter;
import lombok.Setter;
Expand Down Expand Up @@ -65,6 +68,13 @@ public TestkitResponse process(TestkitState testkitState) {
bookmarkManagerId.equals("-1") ? null : testkitState.getBookmarkManager(bookmarkManagerId);
configBuilder.withBookmarkManager(bookmarkManager);
}

Optional.ofNullable(data.getConfig().getTimeout())
.map(Duration::ofMillis)
.ifPresent(configBuilder::withTimeout);

Optional.ofNullable(data.getConfig().getTxMeta()).ifPresent(configBuilder::withMetadata);

var params = data.getParams() != null ? data.getParams() : Collections.<String, Object>emptyMap();
var eagerResult = driver.executableQuery(data.getCypher())
.withParameters(params)
Expand Down Expand Up @@ -123,5 +133,9 @@ public static class QueryConfigData {
private String routing;
private String impersonatedUser;
private String bookmarkManagerId;
private Long timeout;

@JsonDeserialize(using = TestkitCypherParamDeserializer.class)
private Map<String, Serializable> txMeta;
}
}