Skip to content

Commit 47f0b0c

Browse files
committed
Add support for lock wait timeout.
[closes #214] Signed-off-by: Mark Paluch <mpaluch@vmware.com>
1 parent 26e9ce5 commit 47f0b0c

9 files changed

+179
-21
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ Mono<Connection> connectionMono = Mono.from(connectionFactory.create());
7777
| `connectionId` | Connection Id for tracing purposes. Defaults to a random Id. _(Optional)_
7878
| `connectTimeout` | Connection Id for tracing purposes. Defaults to 30 seconds. _(Optional)_
7979
| `hostNameInCertificate` | Expected hostname in SSL certificate. Supports wildcards (e.g. `*.database.windows.net`). _(Optional)_
80-
| `preferCursoredExecution` | Whether to prefer cursors or direct execution for queries. Uses by default direct. Cursors require more round-trips but are more backpressure-friendly. Defaults to direct execution. Can be `boolean` or a `Predicate<String>` accepting the SQL query. _(Optional)_
80+
| `lockWaitTimeout` | Lock wait timeout using `SET LOCK_TIMEOUT …`. _(Optional)_
81+
| `preferCursoredExecution` | Whether to prefer cursors or direct execution for queries. Uses by default direct. Cursors require more round-trips but are more backpressure-friendly. Defaults to direct execution. Can be `boolean` or a `Predicate<String>` accepting the SQL query. _(
82+
Optional)_
8183
| `sendStringParametersAsUnicode` | Configure whether to send character data as unicode (NVARCHAR, NCHAR, NTEXT) or whether to use the database encoding, defaults to `true`. If disabled, `CharSequence` data is sent using the database-specific collation such as ASCII/MBCS instead of Unicode.
8284
| `sslTunnel` | Enables SSL tunnel usage when using a SSL tunnel or SSL terminator in front of SQL Server. Accepts `Function<SslContextBuilder, SslContextBuilder>` to customize the SSL tunnel settings. SSL tunneling is not related to SQL Server's built-in SSL support. _(Optional)_
8385
| `sslContextBuilderCustomizer` | SSL Context customizer to configure SQL Server's built-in SSL support (`Function<SslContextBuilder, SslContextBuilder>`) _(Optional)_

src/main/java/io/r2dbc/mssql/ExceptionFactory.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,10 @@ static R2dbcException createException(AbstractInfoToken token, String sql) {
9999
case 8115: // Arithmetic overflow error converting %ls to data type %ls.
100100
return new MssqlDataIntegrityViolationException(createErrorDetails(token));
101101

102-
case 701: // Maximum number of databases used for each query has been exceeded. The maximum allowed is %d.
103102
case 1222: // Lock request time out period exceeded.
103+
return new MssqlTimeoutException(createErrorDetails(token));
104+
105+
case 701: // Maximum number of databases used for each query has been exceeded. The maximum allowed is %d.
104106
case 1204: // The instance of the SQL Server Database Engine cannot obtain a LOCK resource at this time. Rerun your statement when there are fewer active users. Ask the database
105107
// administrator to check the lock and memory configuration for this instance, or to check for
106108
return new MssqlTransientException(createErrorDetails(token));
@@ -199,7 +201,7 @@ static final class MssqlBadGrammarException extends R2dbcBadGrammarException imp
199201

200202
@Override
201203
public ErrorDetails getErrorDetails() {
202-
return errorDetails;
204+
return this.errorDetails;
203205
}
204206

205207
}
@@ -218,7 +220,7 @@ static final class MssqlDataIntegrityViolationException extends R2dbcDataIntegri
218220

219221
@Override
220222
public ErrorDetails getErrorDetails() {
221-
return errorDetails;
223+
return this.errorDetails;
222224
}
223225

224226
}
@@ -237,7 +239,7 @@ static final class MssqlNonTransientException extends R2dbcNonTransientException
237239

238240
@Override
239241
public ErrorDetails getErrorDetails() {
240-
return errorDetails;
242+
return this.errorDetails;
241243
}
242244

243245
}
@@ -256,7 +258,7 @@ static final class MssqlNonTransientResourceException extends R2dbcNonTransientR
256258

257259
@Override
258260
public ErrorDetails getErrorDetails() {
259-
return errorDetails;
261+
return this.errorDetails;
260262
}
261263

262264
}
@@ -275,7 +277,7 @@ static final class MssqlPermissionDeniedException extends R2dbcPermissionDeniedE
275277

276278
@Override
277279
public ErrorDetails getErrorDetails() {
278-
return errorDetails;
280+
return this.errorDetails;
279281
}
280282

281283
}
@@ -294,7 +296,7 @@ static final class MssqlRollbackException extends R2dbcRollbackException impleme
294296

295297
@Override
296298
public ErrorDetails getErrorDetails() {
297-
return errorDetails;
299+
return this.errorDetails;
298300
}
299301

300302
}
@@ -313,7 +315,7 @@ static final class MssqlTimeoutException extends R2dbcTimeoutException implement
313315

314316
@Override
315317
public ErrorDetails getErrorDetails() {
316-
return errorDetails;
318+
return this.errorDetails;
317319
}
318320

319321
}
@@ -332,7 +334,7 @@ public MssqlTransientException(ErrorDetails errorDetails) {
332334

333335
@Override
334336
public ErrorDetails getErrorDetails() {
335-
return errorDetails;
337+
return this.errorDetails;
336338
}
337339

338340
}
@@ -351,7 +353,7 @@ private static final class MssqlTransientResourceException extends R2dbcTransien
351353

352354
@Override
353355
public ErrorDetails getErrorDetails() {
354-
return errorDetails;
356+
return this.errorDetails;
355357
}
356358

357359
}

src/main/java/io/r2dbc/mssql/MssqlConnection.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,13 +313,23 @@ public Mono<Void> setAutoCommit(boolean autoCommit) {
313313
});
314314
}
315315

316+
/**
317+
* Configure the lock wait timeout via {@code SET LOCK_TIMEOUT}. {@link Duration#isNegative() Negative values} are translated to {@code -1} meaning infinite wait.
318+
*
319+
* @param timeout
320+
* @return
321+
* @since 0.9
322+
*/
316323
@Override
317-
public Publisher<Void> setLockWaitTimeout(Duration timeout) {
318-
throw new UnsupportedOperationException("https://github.com/r2dbc/r2dbc-mssql/issues/214");
324+
public Mono<Void> setLockWaitTimeout(Duration timeout) {
325+
326+
Assert.requireNonNull(timeout, "Timeout must not be null");
327+
328+
return exchange("SET LOCK_TIMEOUT " + (timeout.isNegative() ? "-1" : "" + timeout.toMillis()));
319329
}
320330

321331
@Override
322-
public Publisher<Void> setStatementTimeout(Duration timeout) {
332+
public Mono<Void> setStatementTimeout(Duration timeout) {
323333
throw new UnsupportedOperationException("https://github.com/r2dbc/r2dbc-mssql/issues/213");
324334
}
325335

src/main/java/io/r2dbc/mssql/MssqlConnectionConfiguration.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ public final class MssqlConnectionConfiguration {
8686

8787
private final Predicate<String> preferCursoredExecution;
8888

89+
@Nullable
90+
private final Duration lockWaitTimeout;
91+
8992
private final int port;
9093

9194
private final boolean sendStringParametersAsUnicode;
@@ -115,7 +118,8 @@ public final class MssqlConnectionConfiguration {
115118
private final String username;
116119

117120
private MssqlConnectionConfiguration(@Nullable String applicationName, @Nullable UUID connectionId, Duration connectTimeout, @Nullable String database, String host, String hostNameInCertificate,
118-
CharSequence password, Predicate<String> preferCursoredExecution, int port, boolean sendStringParametersAsUnicode, boolean ssl,
121+
@Nullable Duration lockWaitTimeout, CharSequence password, Predicate<String> preferCursoredExecution, int port, boolean sendStringParametersAsUnicode,
122+
boolean ssl,
119123
Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer,
120124
@Nullable Function<SslContextBuilder, SslContextBuilder> sslTunnelSslContextBuilderCustomizer, boolean tcpKeepAlive, boolean tcpNoDelay,
121125
boolean trustServerCertificate, @Nullable File trustStore, @Nullable String trustStoreType,
@@ -127,6 +131,7 @@ private MssqlConnectionConfiguration(@Nullable String applicationName, @Nullable
127131
this.database = database;
128132
this.host = Assert.requireNonNull(host, "host must not be null");
129133
this.hostNameInCertificate = Assert.requireNonNull(hostNameInCertificate, "hostNameInCertificate must not be null");
134+
this.lockWaitTimeout = lockWaitTimeout;
130135
this.password = Assert.requireNonNull(password, "password must not be null");
131136
this.preferCursoredExecution = Assert.requireNonNull(preferCursoredExecution, "preferCursoredExecution must not be null");
132137
this.port = port;
@@ -175,7 +180,8 @@ MssqlConnectionConfiguration withRedirect(Redirect redirect) {
175180
}
176181
}
177182

178-
return new MssqlConnectionConfiguration(this.applicationName, this.connectionId, this.connectTimeout, this.database, redirectServerName, hostNameInCertificate, this.password,
183+
return new MssqlConnectionConfiguration(this.applicationName, this.connectionId, this.connectTimeout, this.database, redirectServerName, hostNameInCertificate, this.lockWaitTimeout,
184+
this.password,
179185
this.preferCursoredExecution, redirect.getPort(), this.sendStringParametersAsUnicode, this.ssl, this.sslContextBuilderCustomizer,
180186
this.sslTunnelSslContextBuilderCustomizer, this.tcpKeepAlive, this.tcpNoDelay, this.trustServerCertificate, this.trustStore, this.trustStoreType, this.trustStorePassword, this.username);
181187
}
@@ -199,6 +205,7 @@ public String toString() {
199205
sb.append(", database=\"").append(this.database).append('\"');
200206
sb.append(", host=\"").append(this.host).append('\"');
201207
sb.append(", hostNameInCertificate=\"").append(this.hostNameInCertificate).append('\"');
208+
sb.append(", lockWaitTimeout=\"").append(this.lockWaitTimeout).append('\"');
202209
sb.append(", password=\"").append(repeat(this.password.length(), "*")).append('\"');
203210
sb.append(", preferCursoredExecution=\"").append(this.preferCursoredExecution).append('\"');
204211
sb.append(", port=").append(this.port);
@@ -243,6 +250,11 @@ String getHostNameInCertificate() {
243250
return this.hostNameInCertificate;
244251
}
245252

253+
@Nullable
254+
Duration getLockWaitTimeout() {
255+
return this.lockWaitTimeout;
256+
}
257+
246258
CharSequence getPassword() {
247259
return this.password;
248260
}
@@ -338,6 +350,9 @@ public static final class Builder {
338350

339351
private String hostNameInCertificate;
340352

353+
@Nullable
354+
private Duration lockWaitTimeout;
355+
341356
private Predicate<String> preferCursoredExecution = sql -> false;
342357

343358
private CharSequence password;
@@ -485,6 +500,21 @@ public Builder hostNameInCertificate(String hostNameInCertificate) {
485500
return this;
486501
}
487502

503+
/**
504+
* Configure the lock wait timeout via {@code SET LOCK_TIMEOUT}. {@link Duration#isNegative() Negative values} are translated to {@code -1} meaning infinite wait.
505+
*
506+
* @param timeout the lock wait timeout
507+
* @return this {@link Builder}
508+
* @since 0.9
509+
*/
510+
public Builder lockWaitTimeout(Duration timeout) {
511+
512+
Assert.requireNonNull(timeout, "lock wait timeout must not be null");
513+
514+
this.lockWaitTimeout = timeout;
515+
return this;
516+
}
517+
488518
/**
489519
* Configure the password.
490520
*
@@ -682,7 +712,8 @@ public MssqlConnectionConfiguration build() {
682712
this.hostNameInCertificate = this.host;
683713
}
684714

685-
return new MssqlConnectionConfiguration(this.applicationName, this.connectionId, this.connectTimeout, this.database, this.host, this.hostNameInCertificate, this.password,
715+
return new MssqlConnectionConfiguration(this.applicationName, this.connectionId, this.connectTimeout, this.database, this.host, this.hostNameInCertificate, this.lockWaitTimeout,
716+
this.password,
686717
this.preferCursoredExecution, this.port, this.sendStringParametersAsUnicode, this.ssl, this.sslContextBuilderCustomizer,
687718
this.sslTunnelSslContextBuilderCustomizer, this.tcpKeepAlive,
688719
this.tcpNoDelay, this.trustServerCertificate, this.trustStore,

src/main/java/io/r2dbc/mssql/MssqlConnectionFactory.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import io.r2dbc.spi.ConnectionFactory;
2525
import io.r2dbc.spi.R2dbcNonTransientResourceException;
2626
import io.r2dbc.spi.Row;
27-
import reactor.core.publisher.Flux;
2827
import reactor.core.publisher.Mono;
2928

3029
import java.util.function.Function;
@@ -114,13 +113,17 @@ public Mono<MssqlConnection> create() {
114113
return initializeClient(this.configuration, true)
115114
.flatMap(it -> {
116115

117-
Flux<MssqlConnection> connectionFlux =
116+
Mono<MssqlConnection> connectionMono =
118117
new SimpleMssqlStatement(it, this.connectionOptions, METADATA_QUERY).execute()
119118
.flatMap(result -> result.map((row, rowMetadata) -> toConnectionMetadata(it.getDatabaseVersion().orElse("unknown"), row))).map(metadata -> {
120119
return new MssqlConnection(it, metadata, this.connectionOptions);
121-
});
120+
}).last();
122121

123-
return connectionFlux.last().onErrorResume(throwable -> {
122+
if (this.configuration.getLockWaitTimeout() != null) {
123+
connectionMono = connectionMono.flatMap(connection -> connection.setLockWaitTimeout(this.configuration.getLockWaitTimeout()).thenReturn(connection));
124+
}
125+
126+
return connectionMono.onErrorResume(throwable -> {
124127
return it.close().then(Mono.error(new R2dbcNonTransientResourceException("Cannot connect to " + this.configuration.getHost() + ":" + this.configuration.getPort(), throwable)));
125128
});
126129
});
@@ -130,6 +133,10 @@ private static MssqlConnectionMetadata toConnectionMetadata(String version, Row
130133
return MssqlConnectionMetadata.from(row.get("Edition", String.class), version, row.get("VersionString", String.class));
131134
}
132135

136+
MssqlConnectionConfiguration getConfiguration() {
137+
return this.configuration;
138+
}
139+
133140
ClientConfiguration getClientConfiguration() {
134141
return this.configuration.toClientConfiguration();
135142
}

src/main/java/io/r2dbc/mssql/MssqlConnectionFactoryProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE;
3535
import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;
3636
import static io.r2dbc.spi.ConnectionFactoryOptions.HOST;
37+
import static io.r2dbc.spi.ConnectionFactoryOptions.LOCK_WAIT_TIMEOUT;
3738
import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD;
3839
import static io.r2dbc.spi.ConnectionFactoryOptions.PORT;
3940
import static io.r2dbc.spi.ConnectionFactoryOptions.SSL;
@@ -158,6 +159,7 @@ public MssqlConnectionFactory create(ConnectionFactoryOptions connectionFactoryO
158159
mapper.from(CONNECT_TIMEOUT).map(OptionMapper::toDuration).to(builder::connectTimeout);
159160
mapper.fromTyped(DATABASE).to(builder::database);
160161
mapper.fromTyped(HOSTNAME_IN_CERTIFICATE).to(builder::hostNameInCertificate);
162+
mapper.from(LOCK_WAIT_TIMEOUT).map(OptionMapper::toDuration).to(builder::lockWaitTimeout);
161163
mapper.from(PORT).map(OptionMapper::toInteger).to(builder::port);
162164
mapper.from(PREFER_CURSORED_EXECUTION).map(OptionMapper::toStringPredicate).to(builder::preferCursoredExecution);
163165
mapper.from(SEND_STRING_PARAMETERS_AS_UNICODE).map(OptionMapper::toBoolean).to(builder::sendStringParametersAsUnicode);

src/test/java/io/r2dbc/mssql/MssqlConnectionFactoryProviderTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.junit.jupiter.api.Test;
2323

2424
import java.io.File;
25+
import java.time.Duration;
2526
import java.util.function.Function;
2627
import java.util.function.Predicate;
2728

@@ -37,6 +38,7 @@
3738
import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.TRUST_STORE_TYPE;
3839
import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;
3940
import static io.r2dbc.spi.ConnectionFactoryOptions.HOST;
41+
import static io.r2dbc.spi.ConnectionFactoryOptions.LOCK_WAIT_TIMEOUT;
4042
import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD;
4143
import static io.r2dbc.spi.ConnectionFactoryOptions.PORT;
4244
import static io.r2dbc.spi.ConnectionFactoryOptions.SSL;
@@ -141,6 +143,22 @@ void shouldConfigureWithStaticCursoredExecutionPreference() {
141143
assertThat(options.prefersCursors("foo")).isTrue();
142144
}
143145

146+
@Test
147+
void shouldConfigureWithLockWaitTimeout() {
148+
149+
MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder()
150+
.option(DRIVER, MSSQL_DRIVER)
151+
.option(HOST, "test-host")
152+
.option(PASSWORD, "test-password")
153+
.option(USER, "test-user")
154+
.option(LOCK_WAIT_TIMEOUT, Duration.ofSeconds(10))
155+
.build());
156+
157+
MssqlConnectionConfiguration configuration = factory.getConfiguration();
158+
159+
assertThat(configuration.getLockWaitTimeout()).isEqualTo(Duration.ofSeconds(10));
160+
}
161+
144162
@Test
145163
void shouldConfigureWithStringAsUnicode() {
146164

0 commit comments

Comments
 (0)