Skip to content

Commit 7ec7c4f

Browse files
authored
Add transaction timeout and metadata options to QueryConfig (#1506)
1 parent f2505de commit 7ec7c4f

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
@@ -17,11 +17,16 @@
1717
package org.neo4j.driver;
1818

1919
import static java.util.Objects.requireNonNull;
20+
import static org.neo4j.driver.internal.util.Preconditions.checkArgument;
2021

2122
import java.io.Serial;
2223
import java.io.Serializable;
24+
import java.time.Duration;
25+
import java.util.Collections;
26+
import java.util.Map;
2327
import java.util.Objects;
2428
import java.util.Optional;
29+
import org.neo4j.driver.internal.util.Extract;
2530

2631
/**
2732
* Query configuration used by {@link Driver#executableQuery(String)} and its variants.
@@ -53,6 +58,16 @@ public final class QueryConfig implements Serializable {
5358
* The flag indicating if default bookmark manager should be used.
5459
*/
5560
private final boolean useDefaultBookmarkManager;
61+
/**
62+
* The transaction timeout.
63+
* @since 5.16
64+
*/
65+
private final Duration timeout;
66+
/**
67+
* The transaction metadata.
68+
* @since 5.16
69+
*/
70+
private final Map<String, Serializable> metadata;
5671

5772
/**
5873
* Returns default config value.
@@ -69,6 +84,8 @@ private QueryConfig(Builder builder) {
6984
this.impersonatedUser = builder.impersonatedUser;
7085
this.bookmarkManager = builder.bookmarkManager;
7186
this.useDefaultBookmarkManager = builder.useDefaultBookmarkManager;
87+
this.timeout = builder.timeout;
88+
this.metadata = builder.metadata;
7289
}
7390

7491
/**
@@ -119,6 +136,26 @@ public Optional<BookmarkManager> bookmarkManager(BookmarkManager defaultBookmark
119136
return useDefaultBookmarkManager ? Optional.of(defaultBookmarkManager) : Optional.ofNullable(bookmarkManager);
120137
}
121138

139+
/**
140+
* Get the configured transaction timeout.
141+
*
142+
* @return an {@link Optional} containing the configured timeout or {@link Optional#empty()} otherwise
143+
* @since 5.16
144+
*/
145+
public Optional<Duration> timeout() {
146+
return Optional.ofNullable(timeout);
147+
}
148+
149+
/**
150+
* Get the configured transaction metadata.
151+
*
152+
* @return metadata or empty map when it is not configured
153+
* @since 5.16
154+
*/
155+
public Map<String, Serializable> metadata() {
156+
return metadata;
157+
}
158+
122159
@Override
123160
public boolean equals(Object o) {
124161
if (this == o) return true;
@@ -128,12 +165,15 @@ public boolean equals(Object o) {
128165
&& routing == that.routing
129166
&& Objects.equals(database, that.database)
130167
&& Objects.equals(impersonatedUser, that.impersonatedUser)
131-
&& Objects.equals(bookmarkManager, that.bookmarkManager);
168+
&& Objects.equals(bookmarkManager, that.bookmarkManager)
169+
&& Objects.equals(timeout, that.timeout)
170+
&& Objects.equals(metadata, that.metadata);
132171
}
133172

134173
@Override
135174
public int hashCode() {
136-
return Objects.hash(routing, database, impersonatedUser, bookmarkManager, useDefaultBookmarkManager);
175+
return Objects.hash(
176+
routing, database, impersonatedUser, bookmarkManager, useDefaultBookmarkManager, timeout, metadata);
137177
}
138178

139179
@Override
@@ -143,7 +183,9 @@ public String toString() {
143183
+ database + '\'' + ", impersonatedUser='"
144184
+ impersonatedUser + '\'' + ", bookmarkManager="
145185
+ bookmarkManager + ", useDefaultBookmarkManager="
146-
+ useDefaultBookmarkManager + '}';
186+
+ useDefaultBookmarkManager + '\'' + ", timeout='"
187+
+ timeout + '\'' + ", metadata="
188+
+ metadata + '}';
147189
}
148190

149191
/**
@@ -155,6 +197,8 @@ public static final class Builder {
155197
private String impersonatedUser;
156198
private BookmarkManager bookmarkManager;
157199
private boolean useDefaultBookmarkManager = true;
200+
private Duration timeout;
201+
private Map<String, Serializable> metadata = Collections.emptyMap();
158202

159203
private Builder() {}
160204

@@ -214,6 +258,44 @@ public Builder withBookmarkManager(BookmarkManager bookmarkManager) {
214258
return this;
215259
}
216260

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ public Builder withDefaultTimeout() {
213213
* @param metadata the metadata.
214214
* @return this builder.
215215
*/
216-
public Builder withMetadata(Map<String, Object> metadata) {
216+
public Builder withMetadata(Map<String, ?> metadata) {
217217
requireNonNull(metadata, "Transaction metadata should not be null");
218218
metadata.values()
219219
.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
@@ -80,8 +80,11 @@ public <A, R, T> T execute(Collector<Record, A, R> recordCollector, ResultFinish
8080
return resultFinisher.finish(result.keys(), finishedValue, summary);
8181
};
8282
var accessMode = config.routing().equals(RoutingControl.WRITE) ? AccessMode.WRITE : AccessMode.READ;
83+
var transactionConfigBuilder = TransactionConfig.builder();
84+
config.timeout().ifPresent(transactionConfigBuilder::withTimeout);
85+
transactionConfigBuilder.withMetadata(config.metadata());
8386
return session.execute(
84-
accessMode, txCallback, TransactionConfig.empty(), TelemetryApi.EXECUTABLE_QUERY, false);
87+
accessMode, txCallback, transactionConfigBuilder.build(), TelemetryApi.EXECUTABLE_QUERY, false);
8588
}
8689
}
8790

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

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

24+
import java.io.Serializable;
25+
import java.time.Duration;
26+
import java.util.Collections;
2427
import java.util.List;
28+
import java.util.Map;
29+
import java.util.stream.Stream;
2530
import org.junit.jupiter.api.Test;
2631
import org.junit.jupiter.params.ParameterizedTest;
2732
import org.junit.jupiter.params.provider.MethodSource;
@@ -54,7 +59,6 @@ void shouldUpdateRouting(RoutingControl routing) {
5459
}
5560

5661
@Test
57-
@SuppressWarnings("WriteOnlyObject")
5862
void shouldNotAllowNullRouting() {
5963
assertThrows(NullPointerException.class, () -> QueryConfig.builder().withRouting(null));
6064
}
@@ -100,9 +104,41 @@ void shouldAllowNullBookmarkManager() {
100104
assertTrue(config.bookmarkManager(mock(BookmarkManager.class)).isEmpty());
101105
}
102106

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

@@ -111,6 +147,8 @@ void shouldSerialize() throws Exception {
111147
assertEquals(originalConfig.impersonatedUser(), deserializedConfig.impersonatedUser());
112148
assertEquals(
113149
originalConfig.bookmarkManager(defaultManager), deserializedConfig.bookmarkManager(defaultManager));
150+
assertEquals(originalConfig.timeout(), deserializedConfig.timeout());
151+
assertEquals(originalConfig.metadata(), deserializedConfig.metadata());
114152
}
115153

116154
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
@@ -17,8 +17,11 @@
1717
package neo4j.org.testkit.backend.messages.requests;
1818

1919
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
20+
import java.io.Serializable;
21+
import java.time.Duration;
2022
import java.util.Collections;
2123
import java.util.Map;
24+
import java.util.Optional;
2225
import java.util.concurrent.CompletionStage;
2326
import lombok.Getter;
2427
import lombok.Setter;
@@ -63,6 +66,13 @@ public TestkitResponse process(TestkitState testkitState) {
6366
bookmarkManagerId.equals("-1") ? null : testkitState.getBookmarkManager(bookmarkManagerId);
6467
configBuilder.withBookmarkManager(bookmarkManager);
6568
}
69+
70+
Optional.ofNullable(data.getConfig().getTimeout())
71+
.map(Duration::ofMillis)
72+
.ifPresent(configBuilder::withTimeout);
73+
74+
Optional.ofNullable(data.getConfig().getTxMeta()).ifPresent(configBuilder::withMetadata);
75+
6676
var params = data.getParams() != null ? data.getParams() : Collections.<String, Object>emptyMap();
6777
var eagerResult = driver.executableQuery(data.getCypher())
6878
.withParameters(params)
@@ -121,5 +131,9 @@ public static class QueryConfigData {
121131
private String routing;
122132
private String impersonatedUser;
123133
private String bookmarkManagerId;
134+
private Long timeout;
135+
136+
@JsonDeserialize(using = TestkitCypherParamDeserializer.class)
137+
private Map<String, Serializable> txMeta;
124138
}
125139
}

0 commit comments

Comments
 (0)