diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index afe161d30..982cf0e8e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -3222,6 +3222,21 @@ else if (0 == requestedPacketSize) return this; } + // log open connection failures + private void logConnectFailure(int attemptNumber, SQLServerException e, SQLServerError sqlServerError) { + loggerResiliency.finer(toString() + " Connection open - connection failed on attempt: " + attemptNumber + "."); + + if (e != null) { + loggerResiliency.finer( + toString() + " Connection open - connection failure. Driver error code: " + e.getDriverErrorCode()); + } + + if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) { + loggerResiliency.finer(toString() + " Connection open - connection failure. SQL Server error : " + + sqlServerError.getErrorMessage()); + } + } + /** * This function is used by non failover and failover cases. Even when we make a standard connection the server can * provide us with its FO partner. If no FO information is available a standard connection is made. If the server @@ -3237,6 +3252,7 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu int fedauthRetryInterval = BACKOFF_INTERVAL; // milliseconds to sleep (back off) between attempts. long timeoutUnitInterval; + long timeForFirstTry = 0; // time it took to do 1st try in ms boolean useFailoverHost = false; FailoverInfo tempFailover = null; @@ -3434,34 +3450,24 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu + connectRetryCount + " reached."); } - int errorCode = e.getErrorCode(); - int driverErrorCode = e.getDriverErrorCode(); + // estimate time it took to do 1 try + if (attemptNumber == 0) { + timeForFirstTry = (System.currentTimeMillis() - timerStart); + } + sqlServerError = e.getSQLServerError(); - if (SQLServerException.LOGON_FAILED == errorCode // logon failed, ie bad password - || SQLServerException.PASSWORD_EXPIRED == errorCode // password expired - || SQLServerException.USER_ACCOUNT_LOCKED == errorCode // user account locked - || SQLServerException.DRIVER_ERROR_INVALID_TDS == driverErrorCode // invalid TDS - || SQLServerException.DRIVER_ERROR_SSL_FAILED == driverErrorCode // SSL failure - || SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED == driverErrorCode // TLS1.2 failure - || SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG == driverErrorCode // unsupported config - // (eg Sphinx, invalid - // packetsize, etc) - || (SQLServerException.ERROR_SOCKET_TIMEOUT == driverErrorCode // socket timeout - && (!isDBMirroring || attemptNumber > 0)) // If mirroring, only close after failover has been tried (attempt >= 1) - || timerHasExpired(timerExpire) - // for non-dbmirroring cases, do not retry after tcp socket connection succeeds + if (isFatalError(e) // do not retry on fatal errors + || timerHasExpired(timerExpire) // no time left + || (timerRemaining(timerExpire) < TimeUnit.SECONDS.toMillis(connectRetryInterval) + + 2 * timeForFirstTry) // not enough time for another retry + || (connectRetryCount == 0 && !isDBMirroring && !useTnir) // retries disabled + // retry at least once for TNIR and failover + || (connectRetryCount == 0 && (isDBMirroring || useTnir) && attemptNumber > 0) + || (connectRetryCount != 0 && attemptNumber >= connectRetryCount) // no retries left ) { if (loggerResiliency.isLoggable(Level.FINER)) { - loggerResiliency.finer( - toString() + " Connection open - connection failed on attempt: " + attemptNumber + "."); - loggerResiliency.finer(toString() + " Connection open - connection failure. Driver error code: " - + driverErrorCode); - if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) { - loggerResiliency - .finer(toString() + " Connection open - connection failure. SQL Server error : " - + sqlServerError.getErrorMessage()); - } + logConnectFailure(attemptNumber, e, sqlServerError); } // close the connection and throw the error back @@ -3469,15 +3475,7 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu throw e; } else { if (loggerResiliency.isLoggable(Level.FINER)) { - loggerResiliency.finer( - toString() + " Connection open - connection failed on attempt: " + attemptNumber + "."); - loggerResiliency.finer(toString() + " Connection open - connection failure. Driver error code: " - + driverErrorCode); - if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) { - loggerResiliency - .finer(toString() + " Connection open - connection failure. SQL Server error : " - + sqlServerError.getErrorMessage()); - } + logConnectFailure(attemptNumber, e, sqlServerError); } // Close the TDS channel from the failed connection attempt so that we don't @@ -3496,15 +3494,7 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu if (remainingMilliseconds <= fedauthRetryInterval) { if (loggerResiliency.isLoggable(Level.FINER)) { - loggerResiliency.finer(toString() + " Connection open - connection failed on attempt: " - + attemptNumber + "."); - loggerResiliency.finer(toString() - + " Connection open - connection failure. Driver error code: " + driverErrorCode); - if (null != sqlServerError && !sqlServerError.getErrorMessage().isEmpty()) { - loggerResiliency - .finer(toString() + " Connection open - connection failure. SQL Server error : " - + sqlServerError.getErrorMessage()); - } + logConnectFailure(attemptNumber, e, sqlServerError); } throw e; @@ -3517,19 +3507,23 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu // the network with requests, then update sleep interval for next iteration (max 1 second interval) // We have to sleep for every attempt in case of non-dbMirroring scenarios (including multisubnetfailover), // Whereas for dbMirroring, we sleep for every two attempts as each attempt is to a different server. - if (!isDBMirroring || (1 == attemptNumber % 2)) { - if (loggerResiliency.isLoggable(Level.FINER)) { - loggerResiliency.finer(toString() + " Connection open - sleeping milisec: " + connectRetryInterval); - } - if (loggerResiliency.isLoggable(Level.FINER)) { - loggerResiliency.finer(toString() + " Connection open - connection failed on transient error " - + (sqlServerError != null ? sqlServerError.getErrorNumber() : "") - + ". Wait for connectRetryInterval(" + connectRetryInterval + ")s before retry #" - + attemptNumber); - } + // Make sure there's enough time to do another retry + if (!isDBMirroring || (isDBMirroring && (0 == attemptNumber % 2)) + && (attemptNumber < connectRetryCount && connectRetryCount != 0) + && timerRemaining( + timerExpire) > (TimeUnit.SECONDS.toMillis(connectRetryInterval) + 2 * timeForFirstTry)) { + + // don't wait for TNIR + if (!(useTnir && attemptNumber == 0)) { + if (loggerResiliency.isLoggable(Level.FINER)) { + loggerResiliency.finer(toString() + " Connection open - connection failed on transient error " + + (sqlServerError != null ? sqlServerError.getErrorNumber() : "") + + ". Wait for connectRetryInterval(" + connectRetryInterval + ")s before retry #" + + attemptNumber); + } - sleepForInterval(fedauthRetryInterval); - fedauthRetryInterval = (fedauthRetryInterval < 500) ? fedauthRetryInterval * 2 : 1000; + sleepForInterval(TimeUnit.SECONDS.toMillis(connectRetryInterval)); + } } // Update timeout interval (but no more than the point where we're supposed to fail: timerExpire) @@ -3622,31 +3616,22 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu } } + // non recoverable or retryable fatal errors boolean isFatalError(SQLServerException e) { /* * NOTE: If these conditions are modified, consider modification to conditions in SQLServerConnection::login() * and Reconnect::run() */ + int errorCode = e.getErrorCode(); + int driverErrorCode = e.getDriverErrorCode(); - // actual logon failed (e.g. bad password) - if ((SQLServerException.LOGON_FAILED == e.getErrorCode()) - // actual logon failed (e.g. password expired) - || (SQLServerException.PASSWORD_EXPIRED == e.getErrorCode()) - // actual logon failed (e.g. user account locked) - || (SQLServerException.USER_ACCOUNT_LOCKED == e.getErrorCode()) - // invalid TDS received from server - || (SQLServerException.DRIVER_ERROR_INVALID_TDS == e.getDriverErrorCode()) - // failure negotiating SSL - || (SQLServerException.DRIVER_ERROR_SSL_FAILED == e.getDriverErrorCode()) - // failure TLS1.2 - || (SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED == e.getDriverErrorCode()) - // unsupported configuration (e.g. Sphinx, invalid packet size, etc.) - || (SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG == e.getDriverErrorCode()) - // no more time to try again - || (SQLServerException.ERROR_SOCKET_TIMEOUT == e.getDriverErrorCode())) - return true; - else - return false; + return ((SQLServerException.LOGON_FAILED == errorCode) // logon failed (eg bad password) + || (SQLServerException.PASSWORD_EXPIRED == errorCode) // password expired + || (SQLServerException.USER_ACCOUNT_LOCKED == errorCode) // user account locked + || (SQLServerException.DRIVER_ERROR_INVALID_TDS == driverErrorCode) // invalid TDS from server + || (SQLServerException.DRIVER_ERROR_SSL_FAILED == driverErrorCode) // SSL failure + || (SQLServerException.DRIVER_ERROR_INTERMITTENT_TLS_FAILED == driverErrorCode) // TLS1.2 failure + || (SQLServerException.DRIVER_ERROR_UNSUPPORTED_CONFIG == driverErrorCode)); // unsupported config (eg Sphinx, invalid packet size ,etc) } // reset all params that could have been changed due to ENVCHANGE tokens to defaults, @@ -3707,7 +3692,7 @@ static boolean timerHasExpired(long timerExpire) { } /** - * Get time remaining to timer expiry + * Get time remaining to timer expiry (in ms) * * @param timerExpire * @return remaining time to expiry @@ -5979,7 +5964,7 @@ private SqlAuthenticationToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throw String user = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()); // No of milliseconds to sleep for the initial back off. - int sleepInterval = BACKOFF_INTERVAL; + int fedauthSleepInterval = BACKOFF_INTERVAL; if (!msalContextExists() && !authenticationString.equalsIgnoreCase(SqlAuthentication.ACTIVE_DIRECTORY_INTEGRATED.toString())) { @@ -6078,7 +6063,7 @@ private SqlAuthenticationToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throw int millisecondsRemaining = timerRemaining(timerExpire); if (ActiveDirectoryAuthentication.GET_ACCESS_TOKEN_TRANSIENT_ERROR != errorCategory - || timerHasExpired(timerExpire) || (sleepInterval >= millisecondsRemaining)) { + || timerHasExpired(timerExpire) || (fedauthSleepInterval >= millisecondsRemaining)) { String errorStatus = Integer.toHexString(adalException.getStatus()); @@ -6102,13 +6087,14 @@ private SqlAuthenticationToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throw if (connectionlogger.isLoggable(Level.FINER)) { connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken sleeping: " - + sleepInterval + " milliseconds."); + + fedauthSleepInterval + " milliseconds."); connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken remaining: " + millisecondsRemaining + " milliseconds."); } - sleepForInterval(sleepInterval); - sleepInterval = sleepInterval * 2; + sleepForInterval(fedauthSleepInterval); + fedauthSleepInterval = (fedauthSleepInterval < 500) ? fedauthSleepInterval * 2 : 1000; + } } // else choose MSAL4J for integrated authentication. This option is supported for both windows and unix, diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java index 7d4fa23f0..943804dcf 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java @@ -395,16 +395,17 @@ static String generateStateCode(SQLServerConnection con, int errNum, Integer dat static String checkAndAppendClientConnId(String errMsg, SQLServerConnection conn) { if (null != conn && conn.isConnected()) { UUID clientConnId = conn.getClientConIdInternal(); - assert null != clientConnId; - StringBuilder sb = new StringBuilder(errMsg); - // This syntax of adding connection id is matched in a retry logic. If anything changes here, make - // necessary changes to enableSSL() function's exception handling mechanism. - sb.append(LOG_CLIENT_CONNECTION_ID_PREFIX); - sb.append(clientConnId.toString()); - return sb.toString(); - } else { - return errMsg; + if (null != clientConnId) { + StringBuilder sb = (errMsg != null) ? new StringBuilder(errMsg) : new StringBuilder(); + // This syntax of adding connection id is matched in a retry logic. If anything changes here, make + // necessary changes to enableSSL() function's exception handling mechanism. + sb.append(LOG_CLIENT_CONNECTION_ID_PREFIX); + sb.append(clientConnId.toString()); + return sb.toString(); + } } + return (errMsg != null) ? errMsg : ""; + } static void throwNotSupportedException(SQLServerConnection con, Object obj) throws SQLServerException { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java index eabbbb574..084a2ae9d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java @@ -53,6 +53,7 @@ public class SQLServerConnectionTest extends AbstractTest { // If no retry is done, the function should at least exit in 5 seconds static int threshHoldForNoRetryInMilliseconds = 5000; static int loginTimeOutInSeconds = 10; + static String tnirHost = getConfiguredProperty("tnirHost"); String randomServer = RandomUtil.getIdentifier("Server"); @@ -489,6 +490,44 @@ public void testConnectCountInLoginAndCorrectRetryCount() { } } + // Test connect retry 0 but should still connect to TNIR + @Test + @Tag(Constants.xAzureSQLDW) + @Tag(Constants.xAzureSQLDB) + @Tag(Constants.reqExternalSetup) + public void testConnectTnir() { + org.junit.Assume.assumeTrue(isWindows); + + // no retries but should connect to TNIR (this assumes host is defined in host file + try (Connection con = PrepUtil + .getConnection(connectionString + ";transparentNetworkIPResolution=true;connectRetryCount=0;serverName=" + + tnirHost);) {} catch (Exception e) { + fail(e.getMessage()); + } + } + + // Test connect retry 0 and TNIR disabled + @Test + @Tag(Constants.xAzureSQLDW) + @Tag(Constants.xAzureSQLDB) + @Tag(Constants.reqExternalSetup) + public void testConnectNoTnir() { + org.junit.Assume.assumeTrue(isWindows); + + // no retries no TNIR should fail even tho host is defined in host file + try (Connection con = PrepUtil.getConnection( + connectionString + ";transparentNetworkIPResolution=false;connectRetryCount=0;serverName=" + tnirHost);) { + assertTrue(con == null, TestResource.getResource("R_shouldNotConnect")); + } catch (Exception e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_tcpipConnectionFailed")) + || ((isSqlAzure() || isSqlAzureDW()) + ? e.getMessage().contains( + TestResource.getResource("R_connectTimedOut")) + : false), + e.getMessage()); + } + } + @Test @Tag(Constants.xAzureSQLDW) @Tag(Constants.xAzureSQLDB) @@ -981,7 +1020,9 @@ public void run() { ds.setURL(connectionString); ds.setServerName("invalidServerName" + UUID.randomUUID()); - ds.setLoginTimeout(5); + ds.setLoginTimeout(30); + ds.setConnectRetryCount(3); + ds.setConnectRetryInterval(10); try (Connection con = ds.getConnection()) {} catch (SQLException e) {} } }; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java index af2bda8af..68fbfc8df 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/TimeoutTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.fail; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; @@ -36,67 +35,81 @@ @RunWith(JUnitPlatform.class) -@Tag("slow") public class TimeoutTest extends AbstractTest { static String randomServer = RandomUtil.getIdentifier("Server"); static String waitForDelaySPName = RandomUtil.getIdentifier("waitForDelaySP"); static final int waitForDelaySeconds = 10; - static final int defaultTimeout = 15; // loginTimeout default value + static final int defaultTimeout = 30; // loginTimeout default value @BeforeAll public static void setupTests() throws Exception { setConnection(); } + /* + * TODO: + * The tests below uses a simple interval counting logic to determine whether there was at least 1 retry. + * Given the interval is long enough, then 1 retry should take at least 1 interval long, so if it took < 1 interval, then it assumes there were no retry. However, this only works if TNIR or failover is not enabled since those cases should retry but no wait interval in between. So this interval counting can not detect these cases. + * Note a better and more reliable way would be to check attemptNumber using reflection to determine the number of retries. + */ + + // test default loginTimeout used if not specified in connection string @Test public void testDefaultLoginTimeout() { - long timerEnd = 0; - + long totalTime = 0; long timerStart = System.currentTimeMillis(); - // Try a non existing server and see if the default timeout is 15 seconds - try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + "connectRetryCount=0")) { + + // non existing server and default values to see if took default timeout + try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, defaultTimeout); + // time should be < default loginTimeout + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(defaultTimeout), + "total time: " + totalTime + " default loginTimout: " + TimeUnit.SECONDS.toMillis(defaultTimeout)); } + // test setting loginTimeout value @Test public void testURLLoginTimeout() { - long timerEnd = 0; - int timeout = 10; + long totalTime = 0; + int timeout = 15; long timerStart = System.currentTimeMillis(); + // non existing server and set loginTimeout try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + ";logintimeout=" + timeout)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, timeout); + // time should be < set loginTimeout + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout), + "total time: " + totalTime + " loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout)); } + // test setting timeout in DM @Test public void testDMLoginTimeoutApplied() { - long timerEnd = 0; - int timeout = 10; + long totalTime = 0; + int timeout = 15; DriverManager.setLoginTimeout(timeout); long timerStart = System.currentTimeMillis(); @@ -104,23 +117,26 @@ public void testDMLoginTimeoutApplied() { try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, timeout); + // time should be < DM timeout + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout), + "total time: " + totalTime + " DM loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout)); } + // test that setting in connection string overrides value set in DM @Test public void testDMLoginTimeoutNotApplied() { - long timerEnd = 0; - int timeout = 10; + long totalTime = 0; + int timeout = 15; try { DriverManager.setLoginTimeout(timeout * 3); // 30 seconds long timerStart = System.currentTimeMillis(); @@ -129,134 +145,172 @@ public void testDMLoginTimeoutNotApplied() { .getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + timeout)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; assertTrue( - (e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage() - .contains(TestResource - .getResource("R_connectTimedOut")) - : false), + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, timeout); + + // time should be < connection string loginTimeout + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout), + "total time: " + totalTime + " loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout)); } finally { DriverManager.setLoginTimeout(0); // Default to 0 again } } + // Test connect retry set to 0 (disabled) + @Test + public void testConnectRetryDisable() { + long totalTime = 0; + long timerStart = System.currentTimeMillis(); + int interval = defaultTimeout; // long interval so we can tell if there was a retry + long timeout = defaultTimeout * 2; // long loginTimeout to accommodate the long interval + + // non existent server with long loginTimeout, should return fast if no retries at all + try (Connection con = PrepUtil.getConnection( + "jdbc:sqlserver://" + randomServer + ";transparentNetworkIPResolution=false;loginTimeout=" + timeout + + ";connectRetryCount=0;connectInterval=" + interval)) { + fail(TestResource.getResource("R_shouldNotConnect")); + } catch (Exception e) { + totalTime = System.currentTimeMillis() - timerStart; + + assertTrue( + e.getMessage().matches(TestUtils.formatErrorMsg("R_tcpipConnectionFailed")) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), + e.getMessage()); + } + + // if there was a retry then it would take at least 1 interval long, so if < interval means there were no retries + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(interval), + "total time: " + totalTime + " interval: " + TimeUnit.SECONDS.toMillis(interval)); + } + // Test connect retry for non-existent server with loginTimeout @Test public void testConnectRetryBadServer() { - long timerEnd = 0; + long totalTime = 0; long timerStart = System.currentTimeMillis(); - int loginTimeout = 15; + int timeout = 15; // non existent server with very short loginTimeout, no retry will happen as not a transient error - try (Connection con = PrepUtil - .getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + loginTimeout)) { + try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer + ";loginTimeout=" + timeout)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_tcpipConnectionToHost").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, loginTimeout); + // time should be < loginTimeout set + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(timeout), + "total time: " + totalTime + " loginTimeout: " + TimeUnit.SECONDS.toMillis(timeout)); } // Test connect retry for database error @Test public void testConnectRetryServerError() { - long timerEnd = 0; + long totalTime = 0; long timerStart = System.currentTimeMillis(); + int interval = defaultTimeout; // long interval so we can tell if there was a retry + long timeout = defaultTimeout * 2; // long loginTimeout to accommodate the long interval - // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry - int connectRetryCount = new Random().nextInt(256); - int connectRetryInterval = new Random().nextInt(defaultTimeout) + 1; + // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry 1 time try (Connection con = PrepUtil.getConnection( TestUtils.addOrOverrideProperty(connectionString, "database", RandomUtil.getIdentifier("database")) - + ";logintimeout=" + defaultTimeout + ";connectRetryCount=" + connectRetryCount - + ";connectRetryInterval=" + connectRetryInterval)) { + + ";loginTimeout=" + timeout + ";connectRetryCount=" + 1 + ";connectRetryInterval=" + interval + + ";transparentNetworkIPResolution=false")) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - // connect + all retries should always be <= loginTimeout - verifyTimeout(timerEnd - timerStart, defaultTimeout); + // 1 retry should be at least 1 interval long but < 2 intervals + assertTrue(TimeUnit.SECONDS.toMillis(interval) < totalTime, + "interval: " + TimeUnit.SECONDS.toMillis(interval) + " total time: " + totalTime); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(2 * interval), + "total time: " + totalTime + " 2 * interval: " + TimeUnit.SECONDS.toMillis(interval)); } // Test connect retry for database error using Datasource @Test public void testConnectRetryServerErrorDS() { - long timerEnd = 0; + long totalTime = 0; long timerStart = System.currentTimeMillis(); + int interval = defaultTimeout; // long interval so we can tell if there was a retry + long loginTimeout = defaultTimeout * 2; // long loginTimeout to accommodate the long interval - // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry - int connectRetryCount = new Random().nextInt(256); - int connectRetryInterval = new Random().nextInt(defaultTimeout) + 1; - + // non existent database with interval < loginTimeout this will generate a 4060 transient error and retry 1 time SQLServerDataSource ds = new SQLServerDataSource(); String connectStr = TestUtils.addOrOverrideProperty(connectionString, "database", - RandomUtil.getIdentifier("database")) + ";logintimeout=" + defaultTimeout + ";connectRetryCount=" - + connectRetryCount + ";connectRetryInterval=" + connectRetryInterval; + RandomUtil.getIdentifier("database")) + ";logintimeout=" + loginTimeout + ";connectRetryCount=1" + + ";connectRetryInterval=" + interval; updateDataSource(connectStr, ds); try (Connection con = PrepUtil.getConnection(connectStr)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; } - // connect + all retries should always be <= loginTimeout - verifyTimeout(timerEnd - timerStart, defaultTimeout); + // 1 retry should be at least 1 interval long but < 2 intervals + assertTrue(TimeUnit.SECONDS.toMillis(interval) < totalTime, + "interval: " + TimeUnit.SECONDS.toMillis(interval) + " total time: " + totalTime); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(2 * interval), + "total time: " + totalTime + " 2 * interval: " + TimeUnit.SECONDS.toMillis(2 * interval)); } // Test connect retry for database error with loginTimeout @Test public void testConnectRetryTimeout() { - long timerEnd = 0; + long totalTime = 0; long timerStart = System.currentTimeMillis(); + int interval = defaultTimeout; // long interval so we can tell if there was a retry int loginTimeout = 2; - // non existent database with very short loginTimeout so there is no time to do all retries + // non existent database with very short loginTimeout so there is no time to do any retry try (Connection con = PrepUtil.getConnection( TestUtils.addOrOverrideProperty(connectionString, "database", RandomUtil.getIdentifier("database")) - + "connectRetryCount=" + (new Random().nextInt(256)) + ";connectRetryInterval=" - + (new Random().nextInt(defaultTimeout - 1) + 1) + ";loginTimeout=" + loginTimeout)) { + + "connectRetryCount=" + (new Random().nextInt(256)) + ";connectRetryInterval=" + interval + + ";loginTimeout=" + loginTimeout)) { fail(TestResource.getResource("R_shouldNotConnect")); } catch (Exception e) { - timerEnd = System.currentTimeMillis(); + totalTime = System.currentTimeMillis() - timerStart; - assertTrue((e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), e.getMessage()); } - verifyTimeout(timerEnd - timerStart, loginTimeout); + // if there was a retry then it would take at least 1 interval long, so if < interval means there were no retries + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(interval), + "total time: " + totalTime + " interval: " + TimeUnit.SECONDS.toMillis(interval)); } // Test for detecting Azure server for connection retries @@ -289,80 +343,6 @@ public void testAzureEndpointRetry() { } } - @Test - public void testFailoverInstanceResolution() throws SQLException { - long timerEnd = 0; - long timerStart = System.currentTimeMillis(); - - // Try a non existing server and see if the default timeout is 15 seconds - try (Connection con = PrepUtil - .getConnection("jdbc:sqlserver://" + randomServer + ";databaseName=FailoverDB_abc;failoverPartner=" - + randomServer + "\\foo;user=sa;password=" + RandomUtil.getIdentifier("password"))) { - fail(TestResource.getResource("R_shouldNotConnect")); - } catch (Exception e) { - timerEnd = System.currentTimeMillis(); - - assertTrue((e.getMessage().contains(TestResource.getResource("R_tcpipConnectionToHost"))) - || ((isSqlAzure() || isSqlAzureDW()) - ? e.getMessage().contains( - TestResource.getResource("R_connectTimedOut")) - : false), - e.getMessage()); - } - - verifyTimeout(timerEnd - timerStart, defaultTimeout * 2); - } - - @Test - public void testFOInstanceResolution2() throws SQLException { - long timerEnd = 0; - - long timerStart = System.currentTimeMillis(); - try (Connection con = PrepUtil - .getConnection("jdbc:sqlserver://" + randomServer + "\\fooggg;databaseName=FailoverDB;failoverPartner=" - + randomServer + "\\foo;user=sa;password=" + RandomUtil.getIdentifier("password"))) { - fail(TestResource.getResource("R_shouldNotConnect")); - } catch (Exception e) { - timerEnd = System.currentTimeMillis(); - } - - verifyTimeout(timerEnd - timerStart, defaultTimeout); - } - - /** - * Tests that failover is correctly used after a socket timeout, by confirming total time includes socketTimeout - * for both primary and failover server. - */ - @Test - public void testFailoverInstanceResolutionWithSocketTimeout() { - long timerEnd; - long timerStart = System.currentTimeMillis(); - - try (Connection con = PrepUtil.getConnection("jdbc:sqlserver://" + randomServer - + ";databaseName=FailoverDB;failoverPartner=" + randomServer + "\\foo;user=sa;password=" - + RandomUtil.getIdentifier("password") + ";socketTimeout=" + waitForDelaySeconds)) { - fail(TestResource.getResource("R_shouldNotConnect")); - } catch (Exception e) { - timerEnd = System.currentTimeMillis(); - if (!(e instanceof SQLException)) { - fail(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage()); - } - - // Driver should correctly attempt to connect to db, experience a socketTimeout, attempt to connect to - // failover, and then have another socketTimeout. So, expected total time is 2 x socketTimeout. - long totalTime = timerEnd - timerStart; - long totalExpectedTime = waitForDelaySeconds * 1000L * 2; // We expect 2 * socketTimeout - assertTrue(totalTime >= totalExpectedTime, TestResource.getResource("R_executionNotLong") + "totalTime: " - + totalTime + " expectedTime: " + totalExpectedTime); - } - } - - private void verifyTimeout(long timeDiff, int timeout) { - // Verify that login timeout does not take longer than seconds. - assertTrue(timeDiff < TimeUnit.SECONDS.toMillis(timeout * 2), - "timeout: " + TimeUnit.SECONDS.toMillis(timeout) + " timediff: " + timeDiff); - } - /** * When query timeout occurs, the connection is still usable. *