Skip to content

Commit 0cd01c8

Browse files
authored
Fix connection retries (#2247)
1 parent 729520d commit 0cd01c8

File tree

8 files changed

+180
-196
lines changed

8 files changed

+180
-196
lines changed

src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -726,8 +726,7 @@ final InetSocketAddress open(String host, int port, int timeoutMillis, boolean u
726726
setSocketOptions(tcpSocket, this);
727727

728728
// set SO_TIMEOUT
729-
int socketTimeout = con.getSocketTimeoutMilliseconds();
730-
tcpSocket.setSoTimeout(socketTimeout);
729+
tcpSocket.setSoTimeout(con.getSocketTimeoutMilliseconds());
731730

732731
inputStream = tcpInputStream = new ProxyInputStream(tcpSocket.getInputStream());
733732
outputStream = tcpOutputStream = tcpSocket.getOutputStream();

src/main/java/com/microsoft/sqlserver/jdbc/IdleConnectionResiliency.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import java.lang.Thread.State;
99
import java.util.concurrent.ScheduledFuture;
10+
import java.util.concurrent.TimeUnit;
1011
import java.util.concurrent.atomic.AtomicInteger;
1112
import java.util.logging.Level;
1213

@@ -164,7 +165,10 @@ int getLoginTimeoutSeconds() {
164165

165166
void reconnect(TDSCommand cmd) throws InterruptedException {
166167
reconnectErrorReceived = null;
167-
reconnectThread = new ReconnectThread(this.connection, cmd);
168+
connectRetryCount = this.connection.getRetryCount();
169+
if (connectRetryCount > 0) {
170+
reconnectThread = new ReconnectThread(this.connection, cmd);
171+
}
168172
reconnectThread.start();
169173
reconnectThread.join();
170174
reconnectErrorReceived = reconnectThread.getException();
@@ -450,8 +454,9 @@ public void run() {
450454
}
451455

452456
boolean keepRetrying = true;
457+
long connectRetryInterval = TimeUnit.SECONDS.toMillis(con.getRetryInterval());
453458

454-
while ((connectRetryCount > 0) && (!stopRequested) && keepRetrying) {
459+
while ((connectRetryCount >= 0) && (!stopRequested) && keepRetrying) {
455460
if (loggerExternal.isLoggable(Level.FINER)) {
456461
loggerExternal.finer("Running reconnect for command: " + command.toString() + " ; ConnectRetryCount = "
457462
+ connectRetryCount);
@@ -468,7 +473,7 @@ public void run() {
468473
} else {
469474
try {
470475
if (connectRetryCount > 1) {
471-
Thread.sleep((long) (con.getRetryInterval()) * 1000);
476+
Thread.sleep(connectRetryInterval);
472477
}
473478
} catch (InterruptedException ie) {
474479
// re-interrupt thread
@@ -492,7 +497,7 @@ public void run() {
492497
}
493498
}
494499

495-
if ((connectRetryCount == 0) && (keepRetrying)) {
500+
if ((connectRetryCount <= 0) && (keepRetrying)) {
496501
eReceived = new SQLServerException(SQLServerException.getErrString("R_crClientAllRecoveryAttemptsFailed"),
497502
eReceived);
498503
}

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java

Lines changed: 105 additions & 166 deletions
Large diffs are not rendered by default.

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ static String generateStateCode(SQLServerConnection con, int errNum, Integer dat
393393
* original error string.
394394
*/
395395
static String checkAndAppendClientConnId(String errMsg, SQLServerConnection conn) {
396-
if (null != conn && conn.attachConnId()) {
396+
if (null != conn && conn.isConnected()) {
397397
UUID clientConnId = conn.getClientConIdInternal();
398398
assert null != clientConnId;
399399
StringBuilder sb = new StringBuilder(errMsg);

src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ public void testConnectCountInLoginAndCorrectRetryCount() {
459459

460460
int connectRetryCount = 3;
461461
int connectRetryInterval = 1;
462-
int longLoginTimeout = loginTimeOutInSeconds * 4; // 120 seconds
462+
int longLoginTimeout = loginTimeOutInSeconds * 4;
463463

464464
try {
465465
SQLServerDataSource ds = new SQLServerDataSource();
@@ -474,17 +474,53 @@ public void testConnectCountInLoginAndCorrectRetryCount() {
474474
assertTrue(con == null, TestResource.getResource("R_shouldNotConnect"));
475475
}
476476
} catch (Exception e) {
477-
assertTrue(e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")), e.getMessage());
478477
long totalTime = System.currentTimeMillis() - timerStart;
478+
479+
assertTrue(e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")), e.getMessage());
479480
int expectedMinimumTimeInMillis = (connectRetryCount * connectRetryInterval) * 1000; // 3 seconds
480481

481482
// Minimum time is 0 seconds per attempt and connectRetryInterval * connectRetryCount seconds of interval.
482483
// Maximum is unknown, but is needs to be less than longLoginTimeout or else this is an issue.
483-
assertTrue(totalTime > expectedMinimumTimeInMillis, TestResource.getResource("R_executionNotLong"));
484-
assertTrue(totalTime < (longLoginTimeout * 1000L), TestResource.getResource("R_executionTooLong"));
484+
assertTrue(totalTime > expectedMinimumTimeInMillis, TestResource.getResource("R_executionNotLong")
485+
+ " totalTime: " + totalTime + " expectedTime: " + expectedMinimumTimeInMillis);
486+
assertTrue( totalTime < (longLoginTimeout * 1000L), TestResource.getResource("R_executionTooLong")
487+
+ "totalTime: " + totalTime + " expectedTime: " + expectedMinimumTimeInMillis);
485488
}
486489
}
487490

491+
/**
492+
* Tests whether connectRetryCount and connectRetryInterval are properly respected in the login loop. As well, tests
493+
* that connection is retried the proper number of times. This is for cases with zero retries.
494+
*/
495+
@Test
496+
public void testConnectCountInLoginAndCorrectRetryCountWithZeroRetry() {
497+
long timerStart = 0;
498+
499+
int connectRetryCount = 0;
500+
int connectRetryInterval = 60;
501+
int longLoginTimeout = loginTimeOutInSeconds * 3; // 90 seconds
502+
503+
try {
504+
SQLServerDataSource ds = new SQLServerDataSource();
505+
ds.setURL(connectionString);
506+
ds.setLoginTimeout(longLoginTimeout);
507+
ds.setConnectRetryCount(connectRetryCount);
508+
ds.setConnectRetryInterval(connectRetryInterval);
509+
ds.setDatabaseName(RandomUtil.getIdentifier("DataBase"));
510+
timerStart = System.currentTimeMillis();
511+
512+
try (Connection con = ds.getConnection()) {
513+
assertTrue(con == null, TestResource.getResource("R_shouldNotConnect"));
514+
}
515+
} catch (Exception e) {
516+
assertTrue(e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")), e.getMessage());
517+
long totalTime = System.currentTimeMillis() - timerStart;
518+
519+
// Maximum is unknown, but is needs to be less than longLoginTimeout or else this is an issue.
520+
assertTrue(totalTime < (longLoginTimeout * 1000L), TestResource.getResource("R_executionTooLong"));
521+
}
522+
}
523+
488524
@Test
489525
@Tag(Constants.xAzureSQLDW)
490526
@Tag(Constants.xAzureSQLDB)
@@ -974,10 +1010,9 @@ public void testThreadInterruptedStatus() throws InterruptedException {
9741010
Runnable runnable = new Runnable() {
9751011
public void run() {
9761012
SQLServerDataSource ds = new SQLServerDataSource();
977-
9781013
ds.setURL(connectionString);
979-
ds.setServerName("invalidServerName" + UUID.randomUUID());
980-
ds.setLoginTimeout(5);
1014+
ds.setDatabaseName("invalidDatabase" + UUID.randomUUID());
1015+
ds.setLoginTimeout(30);
9811016
try (Connection con = ds.getConnection()) {} catch (SQLException e) {}
9821017
}
9831018
};
@@ -992,7 +1027,8 @@ public void run() {
9921027
Thread.sleep(8000);
9931028
executor.shutdownNow();
9941029

995-
assertTrue(status && future.isCancelled(), TestResource.getResource("R_threadInterruptNotSet"));
1030+
assertTrue(status && future.isCancelled(), TestResource.getResource("R_threadInterruptNotSet") + " status: "
1031+
+ status + " isCancelled: " + future.isCancelled());
9961032
}
9971033

9981034
/**

src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public class TimeoutTest extends AbstractTest {
3939
static String randomServer = RandomUtil.getIdentifier("Server");
4040
static String waitForDelaySPName = RandomUtil.getIdentifier("waitForDelaySP");
4141
static final int waitForDelaySeconds = 10;
42-
static final int defaultTimeout = 15; // loginTimeout default value
42+
static final int defaultTimeout = 60; // loginTimeout default value
4343

4444
@BeforeAll
4545
public static void setupTests() throws Exception {
@@ -55,13 +55,13 @@ public void testDefaultLoginTimeout() {
5555
try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + "connectRetryCount=0")) {
5656
fail(TestResource.getResource("R_shouldNotConnect"));
5757
} catch (Exception e) {
58+
timerEnd = System.currentTimeMillis();
5859
assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
5960
|| ((isSqlAzure() || isSqlAzureDW())
6061
? e.getMessage().contains(
6162
TestResource.getResource("R_connectTimedOut"))
6263
: false),
6364
e.getMessage());
64-
timerEnd = System.currentTimeMillis();
6565
}
6666

6767
verifyTimeout(timerEnd - timerStart, defaultTimeout);
@@ -77,13 +77,13 @@ public void testURLLoginTimeout() {
7777
try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + ";logintimeout=" + timeout)) {
7878
fail(TestResource.getResource("R_shouldNotConnect"));
7979
} catch (Exception e) {
80+
timerEnd = System.currentTimeMillis();
8081
assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
8182
|| ((isSqlAzure() || isSqlAzureDW())
8283
? e.getMessage().contains(
8384
TestResource.getResource("R_connectTimedOut"))
8485
: false),
8586
e.getMessage());
86-
timerEnd = System.currentTimeMillis();
8787
}
8888

8989
verifyTimeout(timerEnd - timerStart, timeout);
@@ -100,13 +100,13 @@ public void testDMLoginTimeoutApplied() {
100100
try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer)) {
101101
fail(TestResource.getResource("R_shouldNotConnect"));
102102
} catch (Exception e) {
103+
timerEnd = System.currentTimeMillis();
103104
assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
104105
|| ((isSqlAzure() || isSqlAzureDW())
105106
? e.getMessage().contains(
106107
TestResource.getResource("R_connectTimedOut"))
107108
: false),
108109
e.getMessage());
109-
timerEnd = System.currentTimeMillis();
110110
}
111111

112112
verifyTimeout(timerEnd - timerStart, timeout);
@@ -124,6 +124,7 @@ public void testDMLoginTimeoutNotApplied() {
124124
.getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + timeout)) {
125125
fail(TestResource.getResource("R_shouldNotConnect"));
126126
} catch (Exception e) {
127+
timerEnd = System.currentTimeMillis();
127128
assertTrue(
128129
(e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
129130
|| ((isSqlAzure() || isSqlAzureDW())
@@ -132,7 +133,6 @@ public void testDMLoginTimeoutNotApplied() {
132133
.getResource("R_connectTimedOut"))
133134
: false),
134135
e.getMessage());
135-
timerEnd = System.currentTimeMillis();
136136
}
137137
verifyTimeout(timerEnd - timerStart, timeout);
138138
} finally {
@@ -152,13 +152,13 @@ public void testConnectRetryBadServer() {
152152
.getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + loginTimeout)) {
153153
fail(TestResource.getResource("R_shouldNotConnect"));
154154
} catch (Exception e) {
155+
timerEnd = System.currentTimeMillis();
155156
assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
156157
|| ((isSqlAzure() || isSqlAzureDW())
157158
? e.getMessage().contains(
158159
TestResource.getResource("R_connectTimedOut"))
159160
: false),
160161
e.getMessage());
161-
timerEnd = System.currentTimeMillis();
162162
}
163163

164164
verifyTimeout(timerEnd - timerStart, loginTimeout);
@@ -179,13 +179,13 @@ public void testConnectRetryServerError() {
179179
+ ";connectRetryInterval=" + connectRetryInterval)) {
180180
fail(TestResource.getResource("R_shouldNotConnect"));
181181
} catch (Exception e) {
182+
timerEnd = System.currentTimeMillis();
182183
assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")))
183184
|| ((isSqlAzure() || isSqlAzureDW())
184185
? e.getMessage().contains(
185186
TestResource.getResource("R_connectTimedOut"))
186187
: false),
187188
e.getMessage());
188-
timerEnd = System.currentTimeMillis();
189189
}
190190

191191
// connect + all retries should always be <= loginTimeout
@@ -211,13 +211,13 @@ public void testConnectRetryServerErrorDS() {
211211
try (Connection con = PrepUtil.getConnection(connectStr)) {
212212
fail(TestResource.getResource("R_shouldNotConnect"));
213213
} catch (Exception e) {
214+
timerEnd = System.currentTimeMillis();
214215
assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")))
215216
|| ((isSqlAzure() || isSqlAzureDW())
216217
? e.getMessage().contains(
217218
TestResource.getResource("R_connectTimedOut"))
218219
: false),
219220
e.getMessage());
220-
timerEnd = System.currentTimeMillis();
221221
}
222222

223223
// connect + all retries should always be <= loginTimeout
@@ -228,8 +228,8 @@ public void testConnectRetryServerErrorDS() {
228228
@Test
229229
public void testConnectRetryTimeout() {
230230
long timerEnd = 0;
231-
long timerStart = System.currentTimeMillis();
232231
int loginTimeout = 2;
232+
long timerStart = System.currentTimeMillis();
233233

234234
// non existent server with very short loginTimeout so there is no time to do all retries
235235
try (Connection con = PrepUtil.getConnection(
@@ -238,13 +238,13 @@ public void testConnectRetryTimeout() {
238238
+ (new Random().nextInt(defaultTimeout - 1) + 1) + ";loginTimeout=" + loginTimeout)) {
239239
fail(TestResource.getResource("R_shouldNotConnect"));
240240
} catch (Exception e) {
241+
timerEnd = System.currentTimeMillis();
241242
assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")))
242243
|| ((isSqlAzure() || isSqlAzureDW())
243244
? e.getMessage().contains(
244245
TestResource.getResource("R_connectTimedOut"))
245246
: false),
246247
e.getMessage());
247-
timerEnd = System.currentTimeMillis();
248248
}
249249

250250
verifyTimeout(timerEnd - timerStart, loginTimeout);
@@ -260,13 +260,13 @@ public void testFailoverInstanceResolution() throws SQLException {
260260
+ ";databaseName=FailoverDB_abc;failoverPartner=" + randomServer + "\\foo;user=sa;password=pwd;")) {
261261
fail(TestResource.getResource("R_shouldNotConnect"));
262262
} catch (Exception e) {
263+
timerEnd = System.currentTimeMillis();
263264
assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost")))
264265
|| ((isSqlAzure() || isSqlAzureDW())
265266
? e.getMessage().contains(
266267
TestResource.getResource("R_connectTimedOut"))
267268
: false),
268269
e.getMessage());
269-
timerEnd = System.currentTimeMillis();
270270
}
271271

272272
verifyTimeout(timerEnd - timerStart, defaultTimeout * 2);

src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public void testBasicConnectionAAD() throws Exception {
6464
basicReconnect("jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";user="
6565
+ azureUserName + ";password=" + azurePassword
6666
+ ";loginTimeout=90;Authentication=ActiveDirectoryPassword");
67-
retry = THROTTLE_RETRY_COUNT + 1;
67+
retry = THROTTLE_RETRY_COUNT + 1;
6868
} catch (Exception e) {
6969
if (e.getMessage().matches(TestUtils.formatErrorMsg("R_crClientAllRecoveryAttemptsFailed"))) {
7070
System.out.println(e.getMessage() + ". Recovery failed, retry #" + retry + " in "
@@ -265,7 +265,8 @@ public void testPooledConnectionDB() throws SQLException {
265265
@Test
266266
public void testPooledConnectionLang() throws SQLException {
267267
SQLServerConnectionPoolDataSource mds = new SQLServerConnectionPoolDataSource();
268-
mds.setURL(connectionString);
268+
mds.setURL(connectionString + ";connectRetryCount=1");
269+
269270
PooledConnection pooledConnection = mds.getPooledConnection();
270271
String lang0 = null, lang1 = null;
271272

src/test/java/com/microsoft/sqlserver/jdbc/resiliency/ReflectiveTests.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ private void timeoutVariations(Map<String, String> props, long expectedDuration,
5151
fail("Successfully executed query on a blocked connection.");
5252
} catch (SQLException e) {
5353
double elapsedTime = System.currentTimeMillis() - startTime;
54+
5455
// Timeout should occur after query timeout and not login timeout
55-
assertTrue("Query did not timeout in " + expectedDuration + "ms, elapsed time(ms): " + elapsedTime,
56-
elapsedTime < expectedDuration);
56+
assertTrue("Exception: " + e.getMessage() + ": Query did not timeout in " + expectedDuration
57+
+ "ms, elapsed time(ms): " + elapsedTime, elapsedTime < expectedDuration);
5758
if (expectedErrMsg.isPresent()) {
5859
assertTrue(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage(),
5960
e.getMessage().matches(TestUtils.formatErrorMsg(expectedErrMsg.get())));
@@ -82,6 +83,9 @@ public void testDefaultTimeout() throws SQLException {
8283
public void testDefaultRetry() throws SQLException {
8384
Map<String, String> m = new HashMap<>();
8485
m.put("loginTimeout", "5");
86+
87+
// ensure count is not set to something else as this test assumes exactly just 1 retry
88+
m.put("connectRetryCount", "1");
8589
timeoutVariations(m, 6000, Optional.empty());
8690
}
8791

0 commit comments

Comments
 (0)