Skip to content

Commit e3a4100

Browse files
committed
Add transaction timeout and metadata options to QueryConfig
1 parent fbdd1ad commit e3a4100

File tree

5 files changed

+144
-7
lines changed

5 files changed

+144
-7
lines changed

driver/src/main/java/org/neo4j/driver/QueryConfig.java

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@
1919
package org.neo4j.driver;
2020

2121
import static java.util.Objects.requireNonNull;
22+
import static org.neo4j.driver.internal.util.Preconditions.checkArgument;
2223

2324
import java.io.Serial;
2425
import java.io.Serializable;
26+
import java.time.Duration;
27+
import java.util.Collections;
28+
import java.util.Map;
2529
import java.util.Objects;
2630
import java.util.Optional;
31+
import org.neo4j.driver.internal.util.Extract;
2732

2833
/**
2934
* Query configuration used by {@link Driver#executableQuery(String)} and its variants.
@@ -55,6 +60,16 @@ public final class QueryConfig implements Serializable {
5560
* The flag indicating if default bookmark manager should be used.
5661
*/
5762
private final boolean useDefaultBookmarkManager;
63+
/**
64+
* The transaction timeout.
65+
* @since 5.15
66+
*/
67+
private final Duration timeout;
68+
/**
69+
* The transaction metadata.
70+
* @since 5.15
71+
*/
72+
private final Map<String, Serializable> metadata;
5873

5974
/**
6075
* Returns default config value.
@@ -71,6 +86,8 @@ private QueryConfig(Builder builder) {
7186
this.impersonatedUser = builder.impersonatedUser;
7287
this.bookmarkManager = builder.bookmarkManager;
7388
this.useDefaultBookmarkManager = builder.useDefaultBookmarkManager;
89+
this.timeout = builder.timeout;
90+
this.metadata = builder.metadata;
7491
}
7592

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

141+
/**
142+
* Get the configured transaction timeout.
143+
*
144+
* @return an {@link Optional} containing the configured timeout or {@link Optional#empty()} otherwise
145+
* @since 5.15
146+
*/
147+
public Optional<Duration> timeout() {
148+
return Optional.ofNullable(timeout);
149+
}
150+
151+
/**
152+
* Get the configured transaction metadata.
153+
*
154+
* @return metadata or empty map when it is not configured
155+
* @since 5.15
156+
*/
157+
public Map<String, Serializable> metadata() {
158+
return metadata;
159+
}
160+
124161
@Override
125162
public boolean equals(Object o) {
126163
if (this == o) return true;
@@ -130,12 +167,15 @@ public boolean equals(Object o) {
130167
&& routing == that.routing
131168
&& Objects.equals(database, that.database)
132169
&& Objects.equals(impersonatedUser, that.impersonatedUser)
133-
&& Objects.equals(bookmarkManager, that.bookmarkManager);
170+
&& Objects.equals(bookmarkManager, that.bookmarkManager)
171+
&& Objects.equals(timeout, that.timeout)
172+
&& Objects.equals(metadata, that.metadata);
134173
}
135174

136175
@Override
137176
public int hashCode() {
138-
return Objects.hash(routing, database, impersonatedUser, bookmarkManager, useDefaultBookmarkManager);
177+
return Objects.hash(
178+
routing, database, impersonatedUser, bookmarkManager, useDefaultBookmarkManager, timeout, metadata);
139179
}
140180

141181
@Override
@@ -145,7 +185,9 @@ public String toString() {
145185
+ database + '\'' + ", impersonatedUser='"
146186
+ impersonatedUser + '\'' + ", bookmarkManager="
147187
+ bookmarkManager + ", useDefaultBookmarkManager="
148-
+ useDefaultBookmarkManager + '}';
188+
+ useDefaultBookmarkManager + '\'' + ", timeout='"
189+
+ timeout + '\'' + ", metadata="
190+
+ metadata + '}';
149191
}
150192

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

161205
private Builder() {}
162206

@@ -216,6 +260,44 @@ public Builder withBookmarkManager(BookmarkManager bookmarkManager) {
216260
return this;
217261
}
218262

263+
/**
264+
* Set the transaction timeout. Transactions that execute longer than the configured timeout will be terminated by the database.
265+
* <p>
266+
* This functionality allows user code to limit query/transaction execution time.
267+
* 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).
268+
* 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).
269+
* <p>
270+
* The provided value should not represent a negative duration.
271+
* {@link Duration#ZERO} will make the transaction execute indefinitely.
272+
*
273+
* @param timeout the timeout.
274+
* @return this builder.
275+
* @since 5.15
276+
*/
277+
public Builder withTimeout(Duration timeout) {
278+
if (timeout != null) {
279+
checkArgument(!timeout.isNegative(), "Transaction timeout should not be negative");
280+
}
281+
282+
this.timeout = timeout;
283+
return this;
284+
}
285+
286+
/**
287+
* Set the transaction metadata.
288+
*
289+
* @param metadata the metadata, must not be {@code null}.
290+
* @return this builder.
291+
* @since 5.15
292+
*/
293+
public Builder withMetadata(Map<String, Serializable> metadata) {
294+
requireNonNull(metadata, "Metadata should not be null");
295+
metadata.values()
296+
.forEach(Extract::assertParameter); // Just assert valid parameters but don't create a value map yet
297+
this.metadata = Map.copyOf(metadata); // Create a defensive copy
298+
return this;
299+
}
300+
219301
/**
220302
* Create a config instance from this builder.
221303
*

driver/src/main/java/org/neo4j/driver/TransactionConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ public Builder withDefaultTimeout() {
215215
* @param metadata the metadata.
216216
* @return this builder.
217217
*/
218-
public Builder withMetadata(Map<String, Object> metadata) {
218+
public Builder withMetadata(Map<String, ?> metadata) {
219219
requireNonNull(metadata, "Transaction metadata should not be null");
220220
metadata.values()
221221
.forEach(Extract::assertParameter); // Just assert valid parameters but don't create a value map yet

driver/src/main/java/org/neo4j/driver/internal/InternalExecutableQuery.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,11 @@ public <A, R, T> T execute(Collector<Record, A, R> recordCollector, ResultFinish
8282
return resultFinisher.finish(result.keys(), finishedValue, summary);
8383
};
8484
var accessMode = config.routing().equals(RoutingControl.WRITE) ? AccessMode.WRITE : AccessMode.READ;
85+
var transactionConfigBuilder = TransactionConfig.builder();
86+
config.timeout().ifPresent(transactionConfigBuilder::withTimeout);
87+
transactionConfigBuilder.withMetadata(config.metadata());
8588
return session.execute(
86-
accessMode, txCallback, TransactionConfig.empty(), TelemetryApi.EXECUTABLE_QUERY, false);
89+
accessMode, txCallback, transactionConfigBuilder.build(), TelemetryApi.EXECUTABLE_QUERY, false);
8790
}
8891
}
8992

driver/src/test/java/org/neo4j/driver/QueryConfigTest.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323
import static org.junit.jupiter.api.Assertions.assertTrue;
2424
import static org.mockito.Mockito.mock;
2525

26+
import java.io.Serializable;
27+
import java.time.Duration;
28+
import java.util.Collections;
2629
import java.util.List;
30+
import java.util.Map;
31+
import java.util.stream.Stream;
2732
import org.junit.jupiter.api.Test;
2833
import org.junit.jupiter.params.ParameterizedTest;
2934
import org.junit.jupiter.params.provider.MethodSource;
@@ -56,7 +61,6 @@ void shouldUpdateRouting(RoutingControl routing) {
5661
}
5762

5863
@Test
59-
@SuppressWarnings("WriteOnlyObject")
6064
void shouldNotAllowNullRouting() {
6165
assertThrows(NullPointerException.class, () -> QueryConfig.builder().withRouting(null));
6266
}
@@ -102,9 +106,41 @@ void shouldAllowNullBookmarkManager() {
102106
assertTrue(config.bookmarkManager(mock(BookmarkManager.class)).isEmpty());
103107
}
104108

109+
@Test
110+
void shouldHaveEmptyMetadataByDefault() {
111+
assertEquals(Collections.emptyMap(), QueryConfig.defaultConfig().metadata());
112+
}
113+
114+
@Test
115+
void shouldUpdateMetadata() {
116+
var metadata = Map.<String, Serializable>of("k1", "v1", "k2", 0);
117+
var config = QueryConfig.builder().withMetadata(metadata).build();
118+
119+
assertEquals(metadata, config.metadata());
120+
}
121+
122+
@Test
123+
void shouldHaveNullTimeoutByDefault() {
124+
assertTrue(QueryConfig.defaultConfig().timeout().isEmpty());
125+
}
126+
127+
@ParameterizedTest
128+
@MethodSource("timeoutDurations")
129+
void shouldUpdateTimeout(Duration timeout) {
130+
var config = QueryConfig.builder().withTimeout(timeout).build();
131+
assertEquals(timeout, config.timeout().orElse(null));
132+
}
133+
134+
static Stream<Duration> timeoutDurations() {
135+
return Stream.of(null, Duration.ZERO, Duration.ofMillis(5));
136+
}
137+
105138
@Test
106139
void shouldSerialize() throws Exception {
107-
var originalConfig = QueryConfig.defaultConfig();
140+
var originalConfig = QueryConfig.builder()
141+
.withTimeout(Duration.ofSeconds(1))
142+
.withMetadata(Map.of("k1", "v1", "k2", 1))
143+
.build();
108144
var deserializedConfig = TestUtil.serializeAndReadBack(originalConfig, QueryConfig.class);
109145
var defaultManager = mock(BookmarkManager.class);
110146

@@ -113,6 +149,8 @@ void shouldSerialize() throws Exception {
113149
assertEquals(originalConfig.impersonatedUser(), deserializedConfig.impersonatedUser());
114150
assertEquals(
115151
originalConfig.bookmarkManager(defaultManager), deserializedConfig.bookmarkManager(defaultManager));
152+
assertEquals(originalConfig.timeout(), deserializedConfig.timeout());
153+
assertEquals(originalConfig.metadata(), deserializedConfig.metadata());
116154
}
117155

118156
record ResultWithSummary<T>(T value, ResultSummary summary) {}

testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ExecuteQuery.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919
package neo4j.org.testkit.backend.messages.requests;
2020

2121
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
22+
import java.io.Serializable;
23+
import java.time.Duration;
2224
import java.util.Collections;
2325
import java.util.Map;
26+
import java.util.Optional;
2427
import java.util.concurrent.CompletionStage;
2528
import lombok.Getter;
2629
import lombok.Setter;
@@ -65,6 +68,13 @@ public TestkitResponse process(TestkitState testkitState) {
6568
bookmarkManagerId.equals("-1") ? null : testkitState.getBookmarkManager(bookmarkManagerId);
6669
configBuilder.withBookmarkManager(bookmarkManager);
6770
}
71+
72+
Optional.ofNullable(data.getConfig().getTimeout())
73+
.map(Duration::ofMillis)
74+
.ifPresent(configBuilder::withTimeout);
75+
76+
Optional.ofNullable(data.getConfig().getTxMeta()).ifPresent(configBuilder::withMetadata);
77+
6878
var params = data.getParams() != null ? data.getParams() : Collections.<String, Object>emptyMap();
6979
var eagerResult = driver.executableQuery(data.getCypher())
7080
.withParameters(params)
@@ -123,5 +133,9 @@ public static class QueryConfigData {
123133
private String routing;
124134
private String impersonatedUser;
125135
private String bookmarkManagerId;
136+
private Long timeout;
137+
138+
@JsonDeserialize(using = TestkitCypherParamDeserializer.class)
139+
private Map<String, Serializable> txMeta;
126140
}
127141
}

0 commit comments

Comments
 (0)