From 50f3f18bf2c1d639b15e35e500519c900ac097bb Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 15 Apr 2024 10:19:23 -0700 Subject: [PATCH 01/57] Trying on a new branch b/c I keep getting build failures and I have no idea why... --- .../microsoft/sqlserver/jdbc/ConfigRead.java | 217 ++++++++++++++++++ .../sqlserver/jdbc/ConfigRetryRule.java | 183 +++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java new file mode 100644 index 000000000..db6a39e03 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java @@ -0,0 +1,217 @@ +package com.microsoft.sqlserver.jdbc; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URI; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + + +public class ConfigRead { + private final static int intervalBetweenReads = 30000; // How many ms must have elapsed before we re-read + private final static String defaultPropsFile = "mssql-jdbc.properties"; + private static ConfigRead driverInstance = null; + private static long timeLastModified; + private static long lastTimeRead; + private static String lastQuery = ""; + private static String customRetryRules = ""; // Rules imported from connection string + private static boolean replaceFlag; // Are we replacing the list of transient errors (for connection retry)? + private static HashMap cxnRules = new HashMap<>(); + private static HashMap stmtRules = new HashMap<>(); + + private ConfigRead() throws SQLServerException { + // On instantiation, set last time read and set up rules + lastTimeRead = new Date().getTime(); + setUpRules(); + } + + /** + * Fetches the static instance of ConfigRead, instantiating it if it hasn't already been. + * + * @return The static instance of ConfigRead + * @throws SQLServerException + * an exception + */ + public static synchronized ConfigRead getInstance() throws SQLServerException { + // Every time we fetch this static instance, instantiate if it hasn't been. If it has then re-read and return + // the instance. + if (driverInstance == null) { + driverInstance = new ConfigRead(); + } else { + reread(); + } + + return driverInstance; + } + + /** + * Check if it's time to re-read, and if the file has changed. If so, then re-set up rules. + * + * @throws SQLServerException + * an exception + */ + private static void reread() throws SQLServerException { + long currentTime = new Date().getTime(); + + if ((currentTime - lastTimeRead) >= intervalBetweenReads && !compareModified()) { + lastTimeRead = currentTime; + setUpRules(); + } + } + + private static boolean compareModified() { + String inputToUse = getCurrentClassPath() + defaultPropsFile; + + try { + File f = new File(inputToUse); + return f.lastModified() == timeLastModified; + } catch (Exception e) { + return false; + } + } + + public void setCustomRetryRules(String cRR) throws SQLServerException { + customRetryRules = cRR; + setUpRules(); + } + + public void setFromConnectionString(String custom) throws SQLServerException { + if (!custom.isEmpty()) { + setCustomRetryRules(custom); + } + } + + public void storeLastQuery(String sql) { + lastQuery = sql.toLowerCase(); + } + + public String getLastQuery() { + return lastQuery; + } + + private static void setUpRules() throws SQLServerException { + LinkedList temp = null; + + if (!customRetryRules.isEmpty()) { + // If user as set custom rules in connection string, then we use those over any file + temp = new LinkedList<>(); + for (String s : customRetryRules.split(";")) { + temp.add(s); + } + } else { + try { + temp = readFromFile(); + } catch (IOException e) { + // TODO handle IO exception + } + } + + if (temp != null) { + createRules(temp); + } + } + + private static void createRules(LinkedList list) throws SQLServerException { + cxnRules = new HashMap<>(); + stmtRules = new HashMap<>(); + + for (String temp : list) { + + ConfigRetryRule rule = new ConfigRetryRule(temp); + if (rule.getError().contains(",")) { + + String[] arr = rule.getError().split(","); + + for (String s : arr) { + ConfigRetryRule rulez = new ConfigRetryRule(s, rule); + if (rule.getConnectionStatus()) { + if (rule.getReplaceExisting()) { + cxnRules = new HashMap<>(); + replaceFlag = true; + } + cxnRules.put(Integer.parseInt(rulez.getError()), rulez); + } else { + stmtRules.put(Integer.parseInt(rulez.getError()), rulez); + } + } + } else { + if (rule.getConnectionStatus()) { + if (rule.getReplaceExisting()) { + cxnRules = new HashMap<>(); + replaceFlag = true; + } + cxnRules.put(Integer.parseInt(rule.getError()), rule); + } else { + stmtRules.put(Integer.parseInt(rule.getError()), rule); + } + } + } + } + + private static String getCurrentClassPath() { + try { + String className = new Object() {}.getClass().getEnclosingClass().getName(); + String location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath(); + location = location.substring(0, location.length() - 16); + URI uri = new URI(location + "/"); + return uri.getPath(); + } catch (Exception e) { + // TODO handle exception + } + return null; + } + + private static LinkedList readFromFile() throws IOException { + String filePath = getCurrentClassPath(); + + LinkedList list = new LinkedList<>(); + try { + File f = new File(filePath + defaultPropsFile); + timeLastModified = f.lastModified(); + try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { + String readLine; + + while ((readLine = buffer.readLine()) != null) { + if (readLine.startsWith("retryExec")) { + String value = readLine.split("=")[1]; + for (String s : value.split(";")) { + list.add(s); + } + } + // list.add(readLine); + } + } + } catch (IOException e) { + // TODO handle IO Exception + throw new IOException(); + } + return list; + } + + public ConfigRetryRule searchRuleSet(int ruleToSearch, String ruleSet) throws SQLServerException { + reread(); + if (ruleSet.equals("statement")) { + for (Map.Entry entry : stmtRules.entrySet()) { + if (entry.getKey() == ruleToSearch) { + return entry.getValue(); + } + } + } else { + for (Map.Entry entry : cxnRules.entrySet()) { + if (entry.getKey() == ruleToSearch) { + return entry.getValue(); + } + } + } + + return null; + } + + public boolean getReplaceFlag() { + return replaceFlag; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java new file mode 100644 index 000000000..cc9adcc9e --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java @@ -0,0 +1,183 @@ +package com.microsoft.sqlserver.jdbc; + +import java.text.MessageFormat; +import java.util.ArrayList; + + +public class ConfigRetryRule { + private String retryError; + private String operand = "*"; + private int initialRetryTime = 10; + private int retryChange = 2; + private int retryCount; + private String retryQueries = ""; + private ArrayList waitTimes = new ArrayList<>(); + private boolean isConnection = false; + private boolean replaceExisting = false; + + public ConfigRetryRule(String s) throws SQLServerException { + String[] stArr = parse(s); + addElements(stArr); + calcWaitTime(); + } + + public ConfigRetryRule(String rule, ConfigRetryRule r) { + copyFromCopy(r); + this.retryError = rule; + } + + private void copyFromCopy(ConfigRetryRule r) { + this.retryError = r.getError(); + this.operand = r.getOperand(); + this.initialRetryTime = r.getInitialRetryTime(); + this.retryChange = r.getRetryChange(); + this.retryCount = r.getRetryCount(); + this.retryQueries = r.getRetryQueries(); + this.waitTimes = r.getWaitTimes(); + this.isConnection = r.getConnectionStatus(); + } + + private String[] parse(String s) { + String temp = s + " "; + + temp = temp.replace(": ", ":0"); + temp = temp.replace("{", ""); + temp = temp.replace("}", ""); + temp = temp.trim(); + + // We want to do an empty string check here + + if (temp.isEmpty()) { + + } + + return temp.split(":"); + } + + private void addElements(String[] s) throws SQLServerException { + // +"retryExec={2714,2716:1,2*2:CREATE;2715:1,3;+4060,4070};" + if (s.length == 1) { + // If single element, connection + isConnection = true; + retryError = appendOrReplace(s[0]); + } else if (s.length == 2 || s.length == 3) { + // If 2 or 3, statement, either with or without query + // Parse first element (statement rules) + if (!StringUtils.isNumeric(s[0])) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); + Object[] msgArgs = {s[0], "\"Retry Error\""}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } else { + retryError = s[0]; + } + + // Parse second element (retry options) + String[] st = s[1].split(","); + + // We have retry count AND timing rules + retryCount = Integer.parseInt(st[0]); // TODO input validation + // Second half can either be N, N OP, N OP N + if (st[1].contains("*")) { + // We know its either N OP, N OP N + String[] sss = st[1].split("/*"); + initialRetryTime = Integer.parseInt(sss[0]); + operand = "*"; + if (sss.length > 2) { + retryChange = Integer.parseInt(sss[2]); + } + } else if (st[1].contains("+")) { + // We know its either N OP, N OP N + String[] sss = st[1].split("/+"); + initialRetryTime = Integer.parseInt(sss[0]); + operand = "*"; + if (sss.length > 2) { + retryChange = Integer.parseInt(sss[2]); + } + } else { + initialRetryTime = Integer.parseInt(st[1]); + // TODO set defaults + } + if (s.length == 3) { + // Query has also been provided + retryQueries = (s[2].equals("0") ? "" : s[2].toLowerCase()); + } + } else { + // If the length is not 1,2,3, then the provided option is invalid + // Prov + + StringBuilder builder = new StringBuilder(); + + for (String string : s) { + builder.append(string); + } + + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); + Object[] msgArgs = {builder.toString()}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + } + + private String appendOrReplace(String s) { + if (s.charAt(0) == '+') { + replaceExisting = false; + StringUtils.isNumeric(s.substring(1)); + return s.substring(1); + } else { + replaceExisting = true; + return s; + } + } + + public void calcWaitTime() { + for (int i = 0; i < retryCount; ++i) { + int waitTime = initialRetryTime; + if (operand.equals("+")) { + for (int j = 0; j < i; ++j) { + waitTime += retryChange; + } + } else if (operand.equals("*")) { + for (int k = 0; k < i; ++k) { + waitTime *= retryChange; + } + + } + waitTimes.add(waitTime); + } + } + + public String getError() { + return retryError; + } + + public String getOperand() { + return operand; + } + + public int getInitialRetryTime() { + return initialRetryTime; + } + + public int getRetryChange() { + return retryChange; + } + + public int getRetryCount() { + return retryCount; + } + + public boolean getConnectionStatus() { + return isConnection; + } + + public String getRetryQueries() { + return retryQueries; + } + + public ArrayList getWaitTimes() { + return waitTimes; + } + + public boolean getReplaceExisting() { + return replaceExisting; + } +} From 74e007a0c515eda0dfe0d94f917fd7ecea16b27d Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 15 Apr 2024 13:44:34 -0700 Subject: [PATCH 02/57] Adding back retryExec --- .../sqlserver/jdbc/ISQLServerDataSource.java | 14 ++++++++++++++ .../sqlserver/jdbc/SQLServerDataSource.java | 12 ++++++++++++ .../microsoft/sqlserver/jdbc/SQLServerDriver.java | 5 ++++- .../sqlserver/jdbc/SQLServerResource.java | 1 + .../sqlserver/jdbc/SQLServerConnectionTest.java | 3 +++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java index 823b11d9d..4211c225d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java @@ -1350,4 +1350,18 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { * @return calcBigDecimalPrecision boolean value */ boolean getCalcBigDecimalPrecision(); + + /** + * Returns value of 'retryExec' from Connection String. + * + * @param retryExec + */ + void setRetryExec(String retryExec); + + /** + * Sets the value for 'retryExec' property + * + * @return retryExec String value + */ + String getRetryExec(); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index 7dfffdb51..7a469adae 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -1364,6 +1364,18 @@ public boolean getCalcBigDecimalPrecision() { SQLServerDriverBooleanProperty.CALC_BIG_DECIMAL_PRECISION.getDefaultValue()); } + @Override + public void setRetryExec(String retryExec) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.RETRY_EXEC.toString(), + retryExec); + } + + @Override + public String getRetryExec() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.RETRY_EXEC.toString(), + null); + } + /** * Sets a property string value. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 2c45d1090..69398436b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -610,7 +610,8 @@ enum SQLServerDriverStringProperty { ENCRYPT("encrypt", EncryptOption.TRUE.toString()), SERVER_CERTIFICATE("serverCertificate", ""), DATETIME_DATATYPE("datetimeParameterType", DatetimeType.DATETIME2.toString()), - ACCESS_TOKEN_CALLBACK_CLASS("accessTokenCallbackClass", ""); + ACCESS_TOKEN_CALLBACK_CLASS("accessTokenCallbackClass", ""), + RETRY_EXEC("retryExec",""); private final String name; private final String defaultValue; @@ -855,6 +856,8 @@ public final class SQLServerDriver implements java.sql.Driver { SQLServerDriverObjectProperty.ACCESS_TOKEN_CALLBACK.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.toString(), SQLServerDriverStringProperty.ACCESS_TOKEN_CALLBACK_CLASS.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.RETRY_EXEC.toString(), + SQLServerDriverStringProperty.RETRY_EXEC.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.REPLICATION.toString(), Boolean.toString(SQLServerDriverBooleanProperty.REPLICATION.getDefaultValue()), false, TRUE_FALSE), new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.toString(), diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 28a1b219a..69bdacb47 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -519,6 +519,7 @@ protected Object[][] getContents() { {"R_InvalidCSVQuotes", "Failed to parse the CSV file, verify that the fields are correctly enclosed in double quotes."}, {"R_TokenRequireUrl", "Token credentials require a URL using the HTTPS protocol scheme."}, {"R_calcBigDecimalPrecisionPropertyDescription", "Indicates whether the driver should calculate precision for big decimal values."}, + {"R_retryExecPropertyDescription", "Indicates whether the driver should calculate precision for big decimal values."}, {"R_maxResultBufferPropertyDescription", "Determines maximum amount of bytes that can be read during retrieval of result set"}, {"R_maxResultBufferInvalidSyntax", "Invalid syntax: {0} in maxResultBuffer parameter."}, {"R_maxResultBufferNegativeParameterValue", "MaxResultBuffer must have positive value: {0}."}, diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java index e024e98cb..bf2aabad1 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java @@ -212,6 +212,9 @@ public void testDataSource() throws SQLServerException { ds.setCalcBigDecimalPrecision(booleanPropValue); assertEquals(booleanPropValue, ds.getCalcBigDecimalPrecision(), TestResource.getResource("R_valuesAreDifferent")); + ds.setRetryExec(stringPropValue); + assertEquals(stringPropValue, ds.getRetryExec(), + TestResource.getResource("R_valuesAreDifferent")); ds.setServerCertificate(stringPropValue); assertEquals(stringPropValue, ds.getServerCertificate(), TestResource.getResource("R_valuesAreDifferent")); From 726fc8529f6fc58698d9631ad88beec6d6560e0e Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 15 Apr 2024 16:13:14 -0700 Subject: [PATCH 03/57] More --- .../sqlserver/jdbc/SQLServerConnection.java | 30 +++++++++++++++++-- .../RequestBoundaryMethodsTest.java | 2 ++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 48275d2aa..e82c4af43 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1069,6 +1069,16 @@ public void setCalcBigDecimalPrecision(boolean calcBigDecimalPrecision) { this.calcBigDecimalPrecision = calcBigDecimalPrecision; } + private String retryExec = SQLServerDriverStringProperty.RETRY_EXEC.getDefaultValue(); + + public String getRetryExec() { + return retryExec; + } + + public void setRetryExec(String retryExec) { + this.retryExec = retryExec; + } + /** Session Recovery Object */ private transient IdleConnectionResiliency sessionRecovery = new IdleConnectionResiliency(this); @@ -2005,9 +2015,15 @@ Connection connect(Properties propsIn, SQLServerPooledConnection pooledConnectio } throw e; } else { - // only retry if transient error + // Retry for all errors transient + passed in to CRL SQLServerError sqlServerError = e.getSQLServerError(); - if (!TransientError.isTransientError(sqlServerError)) { + ConfigRetryRule rule = ConfigRead.getInstance().searchRuleSet(sqlServerError.getErrorNumber(), "connection"); + + if (!ConfigRead.getInstance().getReplaceFlag()) { + if (!TransientError.isTransientError(sqlServerError) || rule == null) { + throw e; + } + } else if (rule == null) { throw e; } @@ -2341,6 +2357,16 @@ Connection connectInternal(Properties propsIn, IPAddressPreference.valueOfString(sPropValue).toString()); } + sPropKey = SQLServerDriverStringProperty.RETRY_EXEC.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null == sPropValue) { + sPropValue = SQLServerDriverStringProperty.RETRY_EXEC.getDefaultValue(); + activeConnectionProperties.setProperty(sPropKey, sPropValue); + } + retryExec = sPropValue; + //ConfigRead.getInstance().setCustomRetryRules(sPropValue); + ConfigRead.getInstance().setFromConnectionString(sPropValue); + sPropKey = SQLServerDriverBooleanProperty.CALC_BIG_DECIMAL_PRECISION.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); if (null == sPropValue) { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java index 9c1a7b2d9..0a42f19b3 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java @@ -523,6 +523,8 @@ private List getVerifiedMethodNames() { verifiedMethodNames.add("setCalcBigDecimalPrecision"); verifiedMethodNames.add("registerBeforeReconnectListener"); verifiedMethodNames.add("removeBeforeReconnectListener"); + verifiedMethodNames.add("getRetryExec"); + verifiedMethodNames.add("setRetryExec"); return verifiedMethodNames; } } From b463d81f3cc9c4a153f32da33881be1cdcbf6cad Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Wed, 17 Apr 2024 11:09:38 -0700 Subject: [PATCH 04/57] Missing null check --- .../sqlserver/jdbc/SQLServerConnection.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index e82c4af43..19425008a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -2015,18 +2015,22 @@ Connection connect(Properties propsIn, SQLServerPooledConnection pooledConnectio } throw e; } else { - // Retry for all errors transient + passed in to CRL SQLServerError sqlServerError = e.getSQLServerError(); - ConfigRetryRule rule = ConfigRead.getInstance().searchRuleSet(sqlServerError.getErrorNumber(), "connection"); - if (!ConfigRead.getInstance().getReplaceFlag()) { - if (!TransientError.isTransientError(sqlServerError) || rule == null) { + if (null == sqlServerError) { + throw e; + } else { + ConfigRetryRule rule = ConfigRead.getInstance().searchRuleSet(sqlServerError.getErrorNumber(), "connection"); + + if (null == rule && !ConfigRead.getInstance().getReplaceFlag() + && !TransientError.isTransientError(sqlServerError)) { + // If the error has not been configured in CRL AND we appending to the existing + // list of errors AND this is not a transient error, then we throw. throw e; } - } else if (rule == null) { - throw e; } + // check if there's time to retry, no point to wait if no time left if ((elapsedSeconds + connectRetryInterval) >= loginTimeoutSeconds) { if (connectionlogger.isLoggable(Level.FINEST)) { From 1ffc13e148a2a938262d6fc2f1b14b5bd185f6a1 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 22 Apr 2024 07:40:20 -0700 Subject: [PATCH 05/57] Next to final --- mssql-jdbc.properties | 1 + .../microsoft/sqlserver/jdbc/ConfigRead.java | 217 --------- .../sqlserver/jdbc/ConfigRetryRule.java | 183 -------- .../jdbc/ConfigurableRetryLogic.java | 412 ++++++++++++++++++ .../sqlserver/jdbc/SQLServerConnection.java | 12 +- .../jdbc/SQLServerPreparedStatement.java | 1 + .../sqlserver/jdbc/SQLServerResource.java | 3 + .../sqlserver/jdbc/SQLServerStatement.java | 72 ++- .../jdbc/ConfigurableRetryLogicTest.java | 229 ++++++++++ 9 files changed, 709 insertions(+), 421 deletions(-) create mode 100644 mssql-jdbc.properties delete mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java delete mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java diff --git a/mssql-jdbc.properties b/mssql-jdbc.properties new file mode 100644 index 000000000..1bc17cbd8 --- /dev/null +++ b/mssql-jdbc.properties @@ -0,0 +1 @@ +retryExec={2714,2716:1,2*2:CREATE;2715:1,3;+4060,4070} \ No newline at end of file diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java deleted file mode 100644 index db6a39e03..000000000 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.microsoft.sqlserver.jdbc; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.net.URI; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; - - -public class ConfigRead { - private final static int intervalBetweenReads = 30000; // How many ms must have elapsed before we re-read - private final static String defaultPropsFile = "mssql-jdbc.properties"; - private static ConfigRead driverInstance = null; - private static long timeLastModified; - private static long lastTimeRead; - private static String lastQuery = ""; - private static String customRetryRules = ""; // Rules imported from connection string - private static boolean replaceFlag; // Are we replacing the list of transient errors (for connection retry)? - private static HashMap cxnRules = new HashMap<>(); - private static HashMap stmtRules = new HashMap<>(); - - private ConfigRead() throws SQLServerException { - // On instantiation, set last time read and set up rules - lastTimeRead = new Date().getTime(); - setUpRules(); - } - - /** - * Fetches the static instance of ConfigRead, instantiating it if it hasn't already been. - * - * @return The static instance of ConfigRead - * @throws SQLServerException - * an exception - */ - public static synchronized ConfigRead getInstance() throws SQLServerException { - // Every time we fetch this static instance, instantiate if it hasn't been. If it has then re-read and return - // the instance. - if (driverInstance == null) { - driverInstance = new ConfigRead(); - } else { - reread(); - } - - return driverInstance; - } - - /** - * Check if it's time to re-read, and if the file has changed. If so, then re-set up rules. - * - * @throws SQLServerException - * an exception - */ - private static void reread() throws SQLServerException { - long currentTime = new Date().getTime(); - - if ((currentTime - lastTimeRead) >= intervalBetweenReads && !compareModified()) { - lastTimeRead = currentTime; - setUpRules(); - } - } - - private static boolean compareModified() { - String inputToUse = getCurrentClassPath() + defaultPropsFile; - - try { - File f = new File(inputToUse); - return f.lastModified() == timeLastModified; - } catch (Exception e) { - return false; - } - } - - public void setCustomRetryRules(String cRR) throws SQLServerException { - customRetryRules = cRR; - setUpRules(); - } - - public void setFromConnectionString(String custom) throws SQLServerException { - if (!custom.isEmpty()) { - setCustomRetryRules(custom); - } - } - - public void storeLastQuery(String sql) { - lastQuery = sql.toLowerCase(); - } - - public String getLastQuery() { - return lastQuery; - } - - private static void setUpRules() throws SQLServerException { - LinkedList temp = null; - - if (!customRetryRules.isEmpty()) { - // If user as set custom rules in connection string, then we use those over any file - temp = new LinkedList<>(); - for (String s : customRetryRules.split(";")) { - temp.add(s); - } - } else { - try { - temp = readFromFile(); - } catch (IOException e) { - // TODO handle IO exception - } - } - - if (temp != null) { - createRules(temp); - } - } - - private static void createRules(LinkedList list) throws SQLServerException { - cxnRules = new HashMap<>(); - stmtRules = new HashMap<>(); - - for (String temp : list) { - - ConfigRetryRule rule = new ConfigRetryRule(temp); - if (rule.getError().contains(",")) { - - String[] arr = rule.getError().split(","); - - for (String s : arr) { - ConfigRetryRule rulez = new ConfigRetryRule(s, rule); - if (rule.getConnectionStatus()) { - if (rule.getReplaceExisting()) { - cxnRules = new HashMap<>(); - replaceFlag = true; - } - cxnRules.put(Integer.parseInt(rulez.getError()), rulez); - } else { - stmtRules.put(Integer.parseInt(rulez.getError()), rulez); - } - } - } else { - if (rule.getConnectionStatus()) { - if (rule.getReplaceExisting()) { - cxnRules = new HashMap<>(); - replaceFlag = true; - } - cxnRules.put(Integer.parseInt(rule.getError()), rule); - } else { - stmtRules.put(Integer.parseInt(rule.getError()), rule); - } - } - } - } - - private static String getCurrentClassPath() { - try { - String className = new Object() {}.getClass().getEnclosingClass().getName(); - String location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath(); - location = location.substring(0, location.length() - 16); - URI uri = new URI(location + "/"); - return uri.getPath(); - } catch (Exception e) { - // TODO handle exception - } - return null; - } - - private static LinkedList readFromFile() throws IOException { - String filePath = getCurrentClassPath(); - - LinkedList list = new LinkedList<>(); - try { - File f = new File(filePath + defaultPropsFile); - timeLastModified = f.lastModified(); - try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { - String readLine; - - while ((readLine = buffer.readLine()) != null) { - if (readLine.startsWith("retryExec")) { - String value = readLine.split("=")[1]; - for (String s : value.split(";")) { - list.add(s); - } - } - // list.add(readLine); - } - } - } catch (IOException e) { - // TODO handle IO Exception - throw new IOException(); - } - return list; - } - - public ConfigRetryRule searchRuleSet(int ruleToSearch, String ruleSet) throws SQLServerException { - reread(); - if (ruleSet.equals("statement")) { - for (Map.Entry entry : stmtRules.entrySet()) { - if (entry.getKey() == ruleToSearch) { - return entry.getValue(); - } - } - } else { - for (Map.Entry entry : cxnRules.entrySet()) { - if (entry.getKey() == ruleToSearch) { - return entry.getValue(); - } - } - } - - return null; - } - - public boolean getReplaceFlag() { - return replaceFlag; - } -} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java deleted file mode 100644 index cc9adcc9e..000000000 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.microsoft.sqlserver.jdbc; - -import java.text.MessageFormat; -import java.util.ArrayList; - - -public class ConfigRetryRule { - private String retryError; - private String operand = "*"; - private int initialRetryTime = 10; - private int retryChange = 2; - private int retryCount; - private String retryQueries = ""; - private ArrayList waitTimes = new ArrayList<>(); - private boolean isConnection = false; - private boolean replaceExisting = false; - - public ConfigRetryRule(String s) throws SQLServerException { - String[] stArr = parse(s); - addElements(stArr); - calcWaitTime(); - } - - public ConfigRetryRule(String rule, ConfigRetryRule r) { - copyFromCopy(r); - this.retryError = rule; - } - - private void copyFromCopy(ConfigRetryRule r) { - this.retryError = r.getError(); - this.operand = r.getOperand(); - this.initialRetryTime = r.getInitialRetryTime(); - this.retryChange = r.getRetryChange(); - this.retryCount = r.getRetryCount(); - this.retryQueries = r.getRetryQueries(); - this.waitTimes = r.getWaitTimes(); - this.isConnection = r.getConnectionStatus(); - } - - private String[] parse(String s) { - String temp = s + " "; - - temp = temp.replace(": ", ":0"); - temp = temp.replace("{", ""); - temp = temp.replace("}", ""); - temp = temp.trim(); - - // We want to do an empty string check here - - if (temp.isEmpty()) { - - } - - return temp.split(":"); - } - - private void addElements(String[] s) throws SQLServerException { - // +"retryExec={2714,2716:1,2*2:CREATE;2715:1,3;+4060,4070};" - if (s.length == 1) { - // If single element, connection - isConnection = true; - retryError = appendOrReplace(s[0]); - } else if (s.length == 2 || s.length == 3) { - // If 2 or 3, statement, either with or without query - // Parse first element (statement rules) - if (!StringUtils.isNumeric(s[0])) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); - Object[] msgArgs = {s[0], "\"Retry Error\""}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, true); - } else { - retryError = s[0]; - } - - // Parse second element (retry options) - String[] st = s[1].split(","); - - // We have retry count AND timing rules - retryCount = Integer.parseInt(st[0]); // TODO input validation - // Second half can either be N, N OP, N OP N - if (st[1].contains("*")) { - // We know its either N OP, N OP N - String[] sss = st[1].split("/*"); - initialRetryTime = Integer.parseInt(sss[0]); - operand = "*"; - if (sss.length > 2) { - retryChange = Integer.parseInt(sss[2]); - } - } else if (st[1].contains("+")) { - // We know its either N OP, N OP N - String[] sss = st[1].split("/+"); - initialRetryTime = Integer.parseInt(sss[0]); - operand = "*"; - if (sss.length > 2) { - retryChange = Integer.parseInt(sss[2]); - } - } else { - initialRetryTime = Integer.parseInt(st[1]); - // TODO set defaults - } - if (s.length == 3) { - // Query has also been provided - retryQueries = (s[2].equals("0") ? "" : s[2].toLowerCase()); - } - } else { - // If the length is not 1,2,3, then the provided option is invalid - // Prov - - StringBuilder builder = new StringBuilder(); - - for (String string : s) { - builder.append(string); - } - - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); - Object[] msgArgs = {builder.toString()}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, true); - } - } - - private String appendOrReplace(String s) { - if (s.charAt(0) == '+') { - replaceExisting = false; - StringUtils.isNumeric(s.substring(1)); - return s.substring(1); - } else { - replaceExisting = true; - return s; - } - } - - public void calcWaitTime() { - for (int i = 0; i < retryCount; ++i) { - int waitTime = initialRetryTime; - if (operand.equals("+")) { - for (int j = 0; j < i; ++j) { - waitTime += retryChange; - } - } else if (operand.equals("*")) { - for (int k = 0; k < i; ++k) { - waitTime *= retryChange; - } - - } - waitTimes.add(waitTime); - } - } - - public String getError() { - return retryError; - } - - public String getOperand() { - return operand; - } - - public int getInitialRetryTime() { - return initialRetryTime; - } - - public int getRetryChange() { - return retryChange; - } - - public int getRetryCount() { - return retryCount; - } - - public boolean getConnectionStatus() { - return isConnection; - } - - public String getRetryQueries() { - return retryQueries; - } - - public ArrayList getWaitTimes() { - return waitTimes; - } - - public boolean getReplaceExisting() { - return replaceExisting; - } -} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java new file mode 100644 index 000000000..9709823ec --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -0,0 +1,412 @@ +package com.microsoft.sqlserver.jdbc; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URI; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + + +public class ConfigurableRetryLogic { + private static ConfigurableRetryLogic driverInstance = null; + private final static int intervalBetweenReads = 30000; // How many ms must have elapsed before we re-read + private final static String defaultPropsFile = "mssql-jdbc.properties"; + private static long timeLastModified; + private static long timeLastRead; + private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) + private static String rulesFromConnectionString = ""; + private static boolean replaceFlag; // Are we replacing the list of transient errors (for connection retry)? + private static HashMap cxnRules = new HashMap<>(); + private static HashMap stmtRules = new HashMap<>(); + static private java.util.logging.Logger configReadLogger = java.util.logging.Logger + .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); + + private ConfigurableRetryLogic() throws SQLServerException { + timeLastRead = new Date().getTime(); + setUpRules(); + } + + /** + * Fetches the static instance of ConfigurableRetryLogic, instantiating it if it hasn't already been. Each time the instance + * is fetched, we check if a re-read is needed, and do so if properties should be re-read. + * + * @return The static instance of ConfigurableRetryLogic + * @throws SQLServerException + * an exception + */ + public static synchronized ConfigurableRetryLogic getInstance() throws SQLServerException { + if (driverInstance == null) { + driverInstance = new ConfigurableRetryLogic(); + } else { + reread(); + } + + return driverInstance; + } + + /** + * Check if it's time to re-read, and if the file has changed. If so, then re-set up rules. + * + * @throws SQLServerException + * an exception + */ + private static void reread() throws SQLServerException { + long currentTime = new Date().getTime(); + + if ((currentTime - timeLastRead) >= intervalBetweenReads && !compareModified()) { + timeLastRead = currentTime; + setUpRules(); + } + } + + private static boolean compareModified() { + String inputToUse = getCurrentClassPath() + defaultPropsFile; + + try { + File f = new File(inputToUse); + return f.lastModified() == timeLastModified; + } catch (Exception e) { + return false; + } + } + + public void setCustomRetryRules(String cRR) throws SQLServerException { + rulesFromConnectionString = cRR; + setUpRules(); + } + + public void setFromConnectionString(String custom) throws SQLServerException { + if (!custom.isEmpty()) { + setCustomRetryRules(custom); + } + } + + public void storeLastQuery(String sql) { + lastQuery = sql.toLowerCase(); + } + + public String getLastQuery() { + return lastQuery; + } + + private static void setUpRules() throws SQLServerException { + LinkedList temp = null; + + if (!rulesFromConnectionString.isEmpty()) { + temp = new LinkedList<>(); + for (String s : rulesFromConnectionString.split(";")) { + temp.add(s); + } + } else { + temp = readFromFile(); + } + + createRules(temp); + } + + private static void createRules(LinkedList listOfRules) throws SQLServerException { + cxnRules = new HashMap<>(); + stmtRules = new HashMap<>(); + + for (String potentialRule : listOfRules) { + ConfigRetryRule rule = new ConfigRetryRule(potentialRule); + + if (rule.getError().contains(",")) { + String[] arr = rule.getError().split(","); + + for (String s : arr) { + ConfigRetryRule splitRule = new ConfigRetryRule(s, rule); + if (rule.getConnectionStatus()) { + if (rule.getReplaceExisting()) { + if (!replaceFlag) { + cxnRules = new HashMap<>(); + } + replaceFlag = true; + } + cxnRules.put(Integer.parseInt(splitRule.getError()), splitRule); + } else { + stmtRules.put(Integer.parseInt(splitRule.getError()), splitRule); + } + } + } else { + if (rule.getConnectionStatus()) { + if (rule.getReplaceExisting()) { + cxnRules = new HashMap<>(); + replaceFlag = true; + } + cxnRules.put(Integer.parseInt(rule.getError()), rule); + } else { + stmtRules.put(Integer.parseInt(rule.getError()), rule); + } + } + } + } + + private static String getCurrentClassPath() { + try { + String className = new Object() {}.getClass().getEnclosingClass().getName(); + String location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath(); + location = location.substring(0, location.length() - 16); + URI uri = new URI(location + "/"); + return uri.getPath(); + } catch (Exception e) { + if (configReadLogger.isLoggable(java.util.logging.Level.FINEST)) { + configReadLogger.finest("Unable to get current class path for properties file reading."); + } + } + return null; + } + + private static LinkedList readFromFile() { + String filePath = getCurrentClassPath(); + + LinkedList list = new LinkedList<>(); + try { + File f = new File(filePath + defaultPropsFile); + timeLastModified = f.lastModified(); + try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { + String readLine; + + while ((readLine = buffer.readLine()) != null) { + if (readLine.startsWith("retryExec")) { + String value = readLine.split("=")[1]; + for (String s : value.split(";")) { + list.add(s); + } + } + } + } + } catch (IOException e) { + if (configReadLogger.isLoggable(java.util.logging.Level.FINEST)) { + configReadLogger.finest("No properties file exists or file is badly formatted."); + } + } + return list; + } + + public ConfigRetryRule searchRuleSet(int ruleToSearch, String ruleSet) throws SQLServerException { + reread(); + if (ruleSet.equals("statement")) { + for (Map.Entry entry : stmtRules.entrySet()) { + if (entry.getKey() == ruleToSearch) { + return entry.getValue(); + } + } + } else { + for (Map.Entry entry : cxnRules.entrySet()) { + if (entry.getKey() == ruleToSearch) { + return entry.getValue(); + } + } + } + + return null; + } + + public boolean getReplaceFlag() { + return replaceFlag; + } +} + + +class ConfigRetryRule { + private String retryError; + private String operand = "+"; + private int initialRetryTime = 0; + private int retryChange = 2; + private int retryCount = 1; + private String retryQueries = ""; + private ArrayList waitTimes = new ArrayList<>(); + private boolean isConnection = false; + private boolean replaceExisting = false; + + public ConfigRetryRule(String s) throws SQLServerException { + String[] stArr = parse(s); + addElements(stArr); + calcWaitTime(); + } + + public ConfigRetryRule(String rule, ConfigRetryRule r) { + copyFromCopy(r); + this.retryError = rule; + } + + private void copyFromCopy(ConfigRetryRule r) { + this.retryError = r.getError(); + this.operand = r.getOperand(); + this.initialRetryTime = r.getInitialRetryTime(); + this.retryChange = r.getRetryChange(); + this.retryCount = r.getRetryCount(); + this.retryQueries = r.getRetryQueries(); + this.waitTimes = r.getWaitTimes(); + this.isConnection = r.getConnectionStatus(); + } + + private String[] parse(String s) { + String temp = s + " "; + + temp = temp.replace(": ", ":0"); + temp = temp.replace("{", ""); + temp = temp.replace("}", ""); + temp = temp.trim(); + + return temp.split(":"); + } + + private void parameterIsNumber(String value) throws SQLServerException { + if (!StringUtils.isNumeric(value)) { + String[] arr = value.split(","); + for (String st : arr) { + if (!StringUtils.isNumeric(st)) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); + throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); + } + } + } + } + + private void addElements(String[] s) throws SQLServerException { + if (s.length == 1) { + String errorWithoutOptionalPrefix = appendOrReplace(s[0]); + parameterIsNumber(errorWithoutOptionalPrefix); + isConnection = true; + retryError = errorWithoutOptionalPrefix; + } else if (s.length == 2 || s.length == 3) { + parameterIsNumber(s[0]); + retryError = s[0]; + + String[] st = s[1].split(","); + parameterIsNumber(st[0]); + if (Integer.parseInt(st[0]) > 0) { + retryCount = Integer.parseInt(st[0]); + } else { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); + throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); + } + + if (st.length == 2) { + if (st[1].contains("*")) { + String[] sss = st[1].split("\\*"); + parameterIsNumber(sss[0]); + initialRetryTime = Integer.parseInt(sss[0]); + operand = "*"; + if (sss.length > 1) { + parameterIsNumber(sss[1]); + retryChange = Integer.parseInt(sss[1]); + } else { + retryChange = initialRetryTime; + } + } else if (st[1].contains("+")) { + String[] sss = st[1].split("\\+"); + parameterIsNumber(sss[0]); + + initialRetryTime = Integer.parseInt(sss[0]); + operand = "+"; + if (sss.length > 1) { + parameterIsNumber(sss[1]); + retryChange = Integer.parseInt(sss[1]); + } + } else { + parameterIsNumber(st[1]); + initialRetryTime = Integer.parseInt(st[1]); + } + } else if (st.length > 2) { + // If the timing options have more than 2 parts, they are badly formatted. + StringBuilder builder = new StringBuilder(); + + for (String string : s) { + builder.append(string); + } + + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); + Object[] msgArgs = {builder.toString()}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + + if (s.length == 3) { + retryQueries = (s[2].equals("0") ? "" : s[2].toLowerCase()); + } + } else { + // If the length is not 1,2,3, then the provided option is invalid + StringBuilder builder = new StringBuilder(); + + for (String string : s) { + builder.append(string); + } + + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); + Object[] msgArgs = {builder.toString()}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + } + + private String appendOrReplace(String s) { + if (s.charAt(0) == '+') { + replaceExisting = false; + StringUtils.isNumeric(s.substring(1)); + return s.substring(1); + } else { + replaceExisting = true; + return s; + } + } + + public void calcWaitTime() { + for (int i = 0; i < retryCount; ++i) { + int waitTime = initialRetryTime; + if (operand.equals("+")) { + for (int j = 0; j < i; ++j) { + waitTime += retryChange; + } + } else if (operand.equals("*")) { + for (int k = 0; k < i; ++k) { + waitTime *= retryChange; + } + + } + waitTimes.add(waitTime); + } + } + + public String getError() { + return retryError; + } + + public String getOperand() { + return operand; + } + + public int getInitialRetryTime() { + return initialRetryTime; + } + + public int getRetryChange() { + return retryChange; + } + + public int getRetryCount() { + return retryCount; + } + + public boolean getConnectionStatus() { + return isConnection; + } + + public String getRetryQueries() { + return retryQueries; + } + + public ArrayList getWaitTimes() { + return waitTimes; + } + + public boolean getReplaceExisting() { + return replaceExisting; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 19425008a..b6be87c70 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -2020,17 +2020,15 @@ Connection connect(Properties propsIn, SQLServerPooledConnection pooledConnectio if (null == sqlServerError) { throw e; } else { - ConfigRetryRule rule = ConfigRead.getInstance().searchRuleSet(sqlServerError.getErrorNumber(), "connection"); + ConfigRetryRule rule = ConfigurableRetryLogic.getInstance() + .searchRuleSet(sqlServerError.getErrorNumber(), "connection"); - if (null == rule && !ConfigRead.getInstance().getReplaceFlag() + if (null == rule && !ConfigurableRetryLogic.getInstance().getReplaceFlag() && !TransientError.isTransientError(sqlServerError)) { - // If the error has not been configured in CRL AND we appending to the existing - // list of errors AND this is not a transient error, then we throw. throw e; } } - // check if there's time to retry, no point to wait if no time left if ((elapsedSeconds + connectRetryInterval) >= loginTimeoutSeconds) { if (connectionlogger.isLoggable(Level.FINEST)) { @@ -2368,8 +2366,8 @@ Connection connectInternal(Properties propsIn, activeConnectionProperties.setProperty(sPropKey, sPropValue); } retryExec = sPropValue; - //ConfigRead.getInstance().setCustomRetryRules(sPropValue); - ConfigRead.getInstance().setFromConnectionString(sPropValue); + // ConfigurableRetryLogic.getInstance().setCustomRetryRules(sPropValue); + ConfigurableRetryLogic.getInstance().setFromConnectionString(sPropValue); sPropKey = SQLServerDriverBooleanProperty.CALC_BIG_DECIMAL_PRECISION.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index f3a441581..03cfac76b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -580,6 +580,7 @@ public boolean execute() throws SQLServerException, SQLTimeoutException { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); + ConfigurableRetryLogic.getInstance().storeLastQuery(this.userSQL); connection.unprepareUnreferencedPreparedStatementHandles(false); executeStatement(new PrepStmtExecCmd(this, EXECUTE)); loggerExternal.exiting(getClassNameLogging(), "execute", null != resultSet); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 69bdacb47..68e1ae9db 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -548,6 +548,9 @@ protected Object[][] getContents() { {"R_InvalidSqlQuery", "Invalid SQL Query: {0}"}, {"R_InvalidScale", "Scale of input value is larger than the maximum allowed by SQL Server."}, {"R_colCountNotMatchColTypeCount", "Number of provided columns {0} does not match the column data types definition {1}."}, + {"R_InvalidParameterFormat", "One or more supplied parameters are not correct."}, + {"R_InvalidRuleFormat", "Wrong number of parameters supplied to rule."}, + {"R_InvalidRetryInterval", "Current retry interval is longer than queryTimeout."}, }; } // @formatter:on diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index f27e1b1ac..24b07a344 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -21,6 +21,7 @@ import java.util.Stack; import java.util.StringTokenizer; import java.util.Vector; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -258,22 +259,64 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ execProps = new ExecuteProperties(this); - try { - // (Re)execute this Statement with the new command - executeCommand(newStmtCmd); - } catch (SQLServerException e) { - if (e.getDriverErrorCode() == SQLServerException.ERROR_QUERY_TIMEOUT) { - if (e.getCause() == null) { - throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e); + boolean cont; + int retryAttempt = 0; + + do { + cont = false; + try { + // (Re)execute this Statement with the new command + executeCommand(newStmtCmd); + } catch (SQLServerException e) { + ConfigRetryRule rule = ConfigurableRetryLogic.getInstance() + .searchRuleSet(e.getSQLServerError().getErrorNumber(), "statement"); + boolean meetsQueryMatch = true; + + if (rule != null && retryAttempt < rule.getRetryCount()) { + if (!(rule.getRetryQueries().isEmpty())) { + // If query has been defined for the rule, we need to query match + meetsQueryMatch = rule.getRetryQueries() + .contains(ConfigurableRetryLogic.getInstance().getLastQuery().split(" ")[0]); + } + + if (meetsQueryMatch) { + try { + int timeToWait = rule.getWaitTimes().get(retryAttempt); + if (connection.getQueryTimeoutSeconds() >= 0 + && timeToWait > connection.getQueryTimeoutSeconds()) { + MessageFormat form = new MessageFormat( + SQLServerException.getErrString("R_InvalidRetryInterval")); + throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); + } + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(timeToWait)); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } catch (IndexOutOfBoundsException exc) { + MessageFormat form = new MessageFormat( + SQLServerException.getErrString("R_indexOutOfRange")); + Object[] msgArgs = {retryAttempt}; + throw new SQLServerException(this, form.format(msgArgs), null, 0, false); + + } + cont = true; + retryAttempt++; + } + } else if (e.getDriverErrorCode() == SQLServerException.ERROR_QUERY_TIMEOUT) { + if (e.getCause() == null) { + throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e); + } + throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e.getCause()); + } else { + throw e; + } + } finally { + if (newStmtCmd.wasExecuted()) { + lastStmtExecCmd = newStmtCmd; } - throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e.getCause()); - } else { - throw e; } - } finally { - if (newStmtCmd.wasExecuted()) - lastStmtExecCmd = newStmtCmd; - } + } while (cont); } /** @@ -810,6 +853,7 @@ public boolean execute(String sql) throws SQLServerException, SQLTimeoutExceptio loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); + ConfigurableRetryLogic.getInstance().storeLastQuery(sql); executeStatement(new StmtExecCmd(this, sql, EXECUTE, NO_GENERATED_KEYS)); loggerExternal.exiting(getClassNameLogging(), "execute", null != resultSet); return null != resultSet; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java new file mode 100644 index 000000000..6c361b6e9 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -0,0 +1,229 @@ +package com.microsoft.sqlserver.jdbc; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; +import com.microsoft.sqlserver.testframework.AbstractTest; + + +/** + * Test connection and statement retry for configurable retry logic + */ +public class ConfigurableRetryLogicTest extends AbstractTest { + private final String baseConnectionString = "jdbc:sqlserver://localhost:1433;database=TestDb;user=sa;password=TestPassword123;" + + "encrypt=true;trustServerCertificate=true;selectMethod=cursor;loginTimeout=5;" + "connectRetryCount=1;"; + private static final String tableName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("crlTestTable")); + + @Test + public void testStatementRetryPS() throws Exception { + String connectionString = baseConnectionString + "retryExec={2714:3,2*2:CREATE;2715:1,3;+4060,4070};"; // 2714 There is already an object named x + + try (Connection conn = DriverManager.getConnection(connectionString); Statement s = conn.createStatement()) { + PreparedStatement ps = conn.prepareStatement("create table " + tableName + " (c1 int null);"); + try { + createTable(s); + ps.execute(); + fail(TestResource.getResource("R_expectedFailPassed")); + } catch (SQLServerException e) { + assertTrue(e.getMessage().startsWith("There is already an object"), + TestResource.getResource("R_unexpectedExceptionContent") + ": " + e.getMessage()); + } finally { + dropTable(s); + } + } + } + + @Test + public void testStatementRetryCS() throws Exception { + String connectionString = baseConnectionString + "retryExec={2714:3,2*2:CREATE;2715:1,3;+4060,4070};"; // 2714 There is already an object named x + String call = "create table " + tableName + " (c1 int null);"; + + try (Connection conn = DriverManager.getConnection(connectionString); Statement s = conn.createStatement(); + CallableStatement cs = conn.prepareCall(call)) { + try { + createTable(s); + cs.execute(); + fail(TestResource.getResource("R_expectedFailPassed")); + } catch (SQLServerException e) { + assertTrue(e.getMessage().startsWith("There is already an object"), + TestResource.getResource("R_unexpectedExceptionContent") + ": " + e.getMessage()); + } finally { + dropTable(s); + } + } + } + + public void testStatementRetry(String connectionString) throws Exception { + String cxnString = baseConnectionString + connectionString; // 2714 There is already an object named x + + try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { + try { + createTable(s); + s.execute("create table " + tableName + " (c1 int null);"); + fail(TestResource.getResource("R_expectedFailPassed")); + } catch (SQLServerException e) { + assertTrue(e.getMessage().startsWith("There is already an object"), + TestResource.getResource("R_unexpectedExceptionContent") + ": " + e.getMessage()); + } finally { + dropTable(s); + } + } + } + + public void testStatementRetryWithShortQueryTimeout(String connectionString) throws Exception { + String cxnString = baseConnectionString + connectionString; // 2714 There is already an object named x + + try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { + try { + createTable(s); + s.execute("create table " + tableName + " (c1 int null);"); + fail(TestResource.getResource("R_expectedFailPassed")); + } finally { + dropTable(s); + } + } + } + + @Test + public void timingTests() { + + } + + @Test + public void readFromFile() { + try { + testStatementRetry(""); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } + } + + @Test + public void testCorrectlyFormattedRulesPass() { + // Correctly formatted rules + try { + // Empty rule set + testStatementRetry("retryExec={};"); + testStatementRetry("retryExec={;};"); + + // Test length 1 + testStatementRetry("retryExec={4060};"); + testStatementRetry("retryExec={+4060,4070};"); + testStatementRetry("retryExec={4060,4070};"); + + testStatementRetry("retryExec={2714:1;};"); + + // Test length 2 + testStatementRetry("retryExec={2714:1,3;};"); + + // Test length 3, also multiple statement errors + testStatementRetry("retryExec={2714,2716:1,2*2:CREATE};"); + // Same as above but using + operator + testStatementRetry("retryExec={2714,2716:1,2+2:CREATE};"); + testStatementRetry("retryExec={2714,2716:1,2+2};"); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } + + // Test length >3 + try { + testStatementRetry("retryExec={2714,2716:1,2*2:CREATE:4};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidRuleFormat"))); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } + } + + @Test + public void testCorrectRetryError() throws Exception { + // Test incorrect format (NaN) + try { + testStatementRetry("retryExec={TEST};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + } + + // Test empty error + try { + testStatementRetry("retryExec={:1,2*2:CREATE};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + } + } + + @Test + public void testProperRetryCount() throws Exception { + // Test min + try { + testStatementRetry("retryExec={2714,2716:-1,2+2:CREATE};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + } + + // Test max (query timeout) + try { + testStatementRetryWithShortQueryTimeout("queryTimeout=10;retryExec={2714,2716:11,2+2:CREATE};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidRetryInterval"))); + } + } + + @Test + public void testProperInitialRetryTime() throws Exception { + // Test min + try { + testStatementRetry("retryExec={2714,2716:4,-1+1:CREATE};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + } + + // Test max + try { + testStatementRetryWithShortQueryTimeout("queryTimeout=3;retryExec={2714,2716:4,100+1:CREATE};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidRetryInterval"))); + } + } + + @Test + public void testProperOperand() throws Exception { + // Test incorrect + try { + testStatementRetry("retryExec={2714,2716:1,2AND2:CREATE};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + } + } + + @Test + public void testProperRetryChange() throws Exception { + // Test incorrect + try { + testStatementRetry("retryExec={2714,2716:1,2+2:CREATE};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + } + } + + private static void createTable(Statement stmt) throws SQLException { + String sql = "create table " + tableName + " (c1 int null);"; + stmt.execute(sql); + } + + private static void dropTable(Statement stmt) throws SQLException { + TestUtils.dropTableIfExists(tableName, stmt); + } +} From d18b02112720c9a2c5b292ae3b7eb3d1c7834173 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 22 Apr 2024 07:40:56 -0700 Subject: [PATCH 06/57] Removed mssql-jdbc.properties --- mssql-jdbc.properties | 1 - 1 file changed, 1 deletion(-) delete mode 100644 mssql-jdbc.properties diff --git a/mssql-jdbc.properties b/mssql-jdbc.properties deleted file mode 100644 index 1bc17cbd8..000000000 --- a/mssql-jdbc.properties +++ /dev/null @@ -1 +0,0 @@ -retryExec={2714,2716:1,2*2:CREATE;2715:1,3;+4060,4070} \ No newline at end of file From 5ff27376c82ebece740b970f88f7bdafbccb0429 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 22 Apr 2024 08:51:13 -0700 Subject: [PATCH 07/57] Set up should start fresh + remove passwords to pass on pipeline --- .../jdbc/ConfigurableRetryLogic.java | 20 +++++++---- .../jdbc/ConfigurableRetryLogicTest.java | 34 +++++++++++-------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 9709823ec..a25b48308 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -76,15 +76,14 @@ private static boolean compareModified() { } } - public void setCustomRetryRules(String cRR) throws SQLServerException { - rulesFromConnectionString = cRR; - setUpRules(); - } +// public void setCustomRetryRules(String cRR) throws SQLServerException { +// rulesFromConnectionString = cRR; +// setUpRules(); +// } public void setFromConnectionString(String custom) throws SQLServerException { - if (!custom.isEmpty()) { - setCustomRetryRules(custom); - } + rulesFromConnectionString = custom; + setUpRules(); } public void storeLastQuery(String sql) { @@ -96,6 +95,12 @@ public String getLastQuery() { } private static void setUpRules() throws SQLServerException { + //For every new setup, everything should be reset + cxnRules = new HashMap<>(); + stmtRules = new HashMap<>(); + replaceFlag = false; + lastQuery = ""; + LinkedList temp = null; if (!rulesFromConnectionString.isEmpty()) { @@ -103,6 +108,7 @@ private static void setUpRules() throws SQLServerException { for (String s : rulesFromConnectionString.split(";")) { temp.add(s); } + rulesFromConnectionString = ""; } else { temp = readFromFile(); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 6c361b6e9..f01067b07 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -11,6 +11,7 @@ import java.sql.Statement; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; @@ -21,16 +22,16 @@ * Test connection and statement retry for configurable retry logic */ public class ConfigurableRetryLogicTest extends AbstractTest { - private final String baseConnectionString = "jdbc:sqlserver://localhost:1433;database=TestDb;user=sa;password=TestPassword123;" - + "encrypt=true;trustServerCertificate=true;selectMethod=cursor;loginTimeout=5;" + "connectRetryCount=1;"; + private static String connectionStringCRL = null; private static final String tableName = AbstractSQLGenerator .escapeIdentifier(RandomUtil.getIdentifier("crlTestTable")); @Test - public void testStatementRetryPS() throws Exception { - String connectionString = baseConnectionString + "retryExec={2714:3,2*2:CREATE;2715:1,3;+4060,4070};"; // 2714 There is already an object named x + public void testStatementRetryPreparedStatement() throws Exception { + connectionStringCRL = TestUtils.addOrOverrideProperty(connectionString, "retryExec", + "{2714:3,2*2:CREATE;2715:1,3;+4060,4070}"); - try (Connection conn = DriverManager.getConnection(connectionString); Statement s = conn.createStatement()) { + try (Connection conn = DriverManager.getConnection(connectionStringCRL); Statement s = conn.createStatement()) { PreparedStatement ps = conn.prepareStatement("create table " + tableName + " (c1 int null);"); try { createTable(s); @@ -46,11 +47,12 @@ public void testStatementRetryPS() throws Exception { } @Test - public void testStatementRetryCS() throws Exception { - String connectionString = baseConnectionString + "retryExec={2714:3,2*2:CREATE;2715:1,3;+4060,4070};"; // 2714 There is already an object named x + public void testStatementRetryCallableStatement() throws Exception { + connectionStringCRL = TestUtils.addOrOverrideProperty(connectionString, "retryExec", + "{2714:3,2*2:CREATE;2715:1,3;+4060,4070}"); String call = "create table " + tableName + " (c1 int null);"; - try (Connection conn = DriverManager.getConnection(connectionString); Statement s = conn.createStatement(); + try (Connection conn = DriverManager.getConnection(connectionStringCRL); Statement s = conn.createStatement(); CallableStatement cs = conn.prepareCall(call)) { try { createTable(s); @@ -65,8 +67,8 @@ public void testStatementRetryCS() throws Exception { } } - public void testStatementRetry(String connectionString) throws Exception { - String cxnString = baseConnectionString + connectionString; // 2714 There is already an object named x + public void testStatementRetry(String addedRetryParams) throws Exception { + String cxnString = connectionString + addedRetryParams; // 2714 There is already an object named x try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { try { @@ -82,8 +84,8 @@ public void testStatementRetry(String connectionString) throws Exception { } } - public void testStatementRetryWithShortQueryTimeout(String connectionString) throws Exception { - String cxnString = baseConnectionString + connectionString; // 2714 There is already an object named x + public void testStatementRetryWithShortQueryTimeout(String addedRetryParams) throws Exception { + String cxnString = connectionString + addedRetryParams; // 2714 There is already an object named x try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { try { @@ -97,7 +99,7 @@ public void testStatementRetryWithShortQueryTimeout(String connectionString) thr } @Test - public void timingTests() { + public void timingTests() throws Exception { } @@ -121,7 +123,6 @@ public void testCorrectlyFormattedRulesPass() { // Test length 1 testStatementRetry("retryExec={4060};"); testStatementRetry("retryExec={+4060,4070};"); - testStatementRetry("retryExec={4060,4070};"); testStatementRetry("retryExec={2714:1;};"); @@ -226,4 +227,9 @@ private static void createTable(Statement stmt) throws SQLException { private static void dropTable(Statement stmt) throws SQLException { TestUtils.dropTableIfExists(tableName, stmt); } + + @BeforeAll + public static void setupTests() throws Exception { + setConnection(); + } } From 9b3863d423b8bd85c757fa7a8b15d7c01519caca Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 22 Apr 2024 09:37:36 -0700 Subject: [PATCH 08/57] Minor cleanup --- .../jdbc/ConfigurableRetryLogic.java | 25 +++++-------------- .../jdbc/ConfigurableRetryLogicTest.java | 2 +- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index a25b48308..21cfa810d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -6,11 +6,7 @@ import java.io.IOException; import java.net.URI; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; +import java.util.*; public class ConfigurableRetryLogic { @@ -24,7 +20,7 @@ public class ConfigurableRetryLogic { private static boolean replaceFlag; // Are we replacing the list of transient errors (for connection retry)? private static HashMap cxnRules = new HashMap<>(); private static HashMap stmtRules = new HashMap<>(); - static private java.util.logging.Logger configReadLogger = java.util.logging.Logger + static private final java.util.logging.Logger configReadLogger = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); private ConfigurableRetryLogic() throws SQLServerException { @@ -76,11 +72,6 @@ private static boolean compareModified() { } } -// public void setCustomRetryRules(String cRR) throws SQLServerException { -// rulesFromConnectionString = cRR; -// setUpRules(); -// } - public void setFromConnectionString(String custom) throws SQLServerException { rulesFromConnectionString = custom; setUpRules(); @@ -101,13 +92,11 @@ private static void setUpRules() throws SQLServerException { replaceFlag = false; lastQuery = ""; - LinkedList temp = null; + LinkedList temp; if (!rulesFromConnectionString.isEmpty()) { temp = new LinkedList<>(); - for (String s : rulesFromConnectionString.split(";")) { - temp.add(s); - } + Collections.addAll(temp, rulesFromConnectionString.split(";")); rulesFromConnectionString = ""; } else { temp = readFromFile(); @@ -182,9 +171,7 @@ private static LinkedList readFromFile() { while ((readLine = buffer.readLine()) != null) { if (readLine.startsWith("retryExec")) { String value = readLine.split("=")[1]; - for (String s : value.split(";")) { - list.add(s); - } + Collections.addAll(list, value.split(";")); } } } @@ -196,7 +183,7 @@ private static LinkedList readFromFile() { return list; } - public ConfigRetryRule searchRuleSet(int ruleToSearch, String ruleSet) throws SQLServerException { + ConfigRetryRule searchRuleSet(int ruleToSearch, String ruleSet) throws SQLServerException { reread(); if (ruleSet.equals("statement")) { for (Map.Entry entry : stmtRules.entrySet()) { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index f01067b07..dd5257612 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -99,7 +99,7 @@ public void testStatementRetryWithShortQueryTimeout(String addedRetryParams) thr } @Test - public void timingTests() throws Exception { + public void timingTests() { } From 16e0224e38490b131397a0711c675dbf3cd502c7 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 22 Apr 2024 09:51:35 -0700 Subject: [PATCH 09/57] Minor cleanup --- .../jdbc/ConfigurableRetryLogic.java | 170 ++++++++---------- 1 file changed, 77 insertions(+), 93 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 21cfa810d..231b54bc7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -6,7 +6,12 @@ import java.io.IOException; import java.net.URI; import java.text.MessageFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; public class ConfigurableRetryLogic { @@ -20,7 +25,7 @@ public class ConfigurableRetryLogic { private static boolean replaceFlag; // Are we replacing the list of transient errors (for connection retry)? private static HashMap cxnRules = new HashMap<>(); private static HashMap stmtRules = new HashMap<>(); - static private final java.util.logging.Logger configReadLogger = java.util.logging.Logger + private static final java.util.logging.Logger configReadLogger = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); private ConfigurableRetryLogic() throws SQLServerException { @@ -42,19 +47,11 @@ public static synchronized ConfigurableRetryLogic getInstance() throws SQLServer } else { reread(); } - return driverInstance; } - /** - * Check if it's time to re-read, and if the file has changed. If so, then re-set up rules. - * - * @throws SQLServerException - * an exception - */ private static void reread() throws SQLServerException { long currentTime = new Date().getTime(); - if ((currentTime - timeLastRead) >= intervalBetweenReads && !compareModified()) { timeLastRead = currentTime; setUpRules(); @@ -72,26 +69,24 @@ private static boolean compareModified() { } } - public void setFromConnectionString(String custom) throws SQLServerException { + void setFromConnectionString(String custom) throws SQLServerException { rulesFromConnectionString = custom; setUpRules(); } - public void storeLastQuery(String sql) { + void storeLastQuery(String sql) { lastQuery = sql.toLowerCase(); } - public String getLastQuery() { + String getLastQuery() { return lastQuery; } private static void setUpRules() throws SQLServerException { - //For every new setup, everything should be reset cxnRules = new HashMap<>(); stmtRules = new HashMap<>(); replaceFlag = false; lastQuery = ""; - LinkedList temp; if (!rulesFromConnectionString.isEmpty()) { @@ -101,7 +96,6 @@ private static void setUpRules() throws SQLServerException { } else { temp = readFromFile(); } - createRules(temp); } @@ -115,8 +109,8 @@ private static void createRules(LinkedList listOfRules) throws SQLServer if (rule.getError().contains(",")) { String[] arr = rule.getError().split(","); - for (String s : arr) { - ConfigRetryRule splitRule = new ConfigRetryRule(s, rule); + for (String retryError : arr) { + ConfigRetryRule splitRule = new ConfigRetryRule(retryError, rule); if (rule.getConnectionStatus()) { if (rule.getReplaceExisting()) { if (!replaceFlag) { @@ -160,14 +154,13 @@ private static String getCurrentClassPath() { private static LinkedList readFromFile() { String filePath = getCurrentClassPath(); - LinkedList list = new LinkedList<>(); + try { File f = new File(filePath + defaultPropsFile); timeLastModified = f.lastModified(); try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { String readLine; - while ((readLine = buffer.readLine()) != null) { if (readLine.startsWith("retryExec")) { String value = readLine.split("=")[1]; @@ -198,11 +191,10 @@ ConfigRetryRule searchRuleSet(int ruleToSearch, String ruleSet) throws SQLServer } } } - return null; } - public boolean getReplaceFlag() { + boolean getReplaceFlag() { return replaceFlag; } } @@ -219,44 +211,43 @@ class ConfigRetryRule { private boolean isConnection = false; private boolean replaceExisting = false; - public ConfigRetryRule(String s) throws SQLServerException { - String[] stArr = parse(s); - addElements(stArr); + public ConfigRetryRule(String rule) throws SQLServerException { + addElements(parse(rule)); calcWaitTime(); } - public ConfigRetryRule(String rule, ConfigRetryRule r) { - copyFromCopy(r); + public ConfigRetryRule(String rule, ConfigRetryRule base) { + copyFromExisting(base); this.retryError = rule; } - private void copyFromCopy(ConfigRetryRule r) { - this.retryError = r.getError(); - this.operand = r.getOperand(); - this.initialRetryTime = r.getInitialRetryTime(); - this.retryChange = r.getRetryChange(); - this.retryCount = r.getRetryCount(); - this.retryQueries = r.getRetryQueries(); - this.waitTimes = r.getWaitTimes(); - this.isConnection = r.getConnectionStatus(); + private void copyFromExisting(ConfigRetryRule base) { + this.retryError = base.getError(); + this.operand = base.getOperand(); + this.initialRetryTime = base.getInitialRetryTime(); + this.retryChange = base.getRetryChange(); + this.retryCount = base.getRetryCount(); + this.retryQueries = base.getRetryQueries(); + this.waitTimes = base.getWaitTimes(); + this.isConnection = base.getConnectionStatus(); } - private String[] parse(String s) { - String temp = s + " "; + private String[] parse(String rule) { + String parsed = rule + " "; - temp = temp.replace(": ", ":0"); - temp = temp.replace("{", ""); - temp = temp.replace("}", ""); - temp = temp.trim(); + parsed = parsed.replace(": ", ":0"); + parsed = parsed.replace("{", ""); + parsed = parsed.replace("}", ""); + parsed = parsed.trim(); - return temp.split(":"); + return parsed.split(":"); } - private void parameterIsNumber(String value) throws SQLServerException { + private void parameterIsNumeric(String value) throws SQLServerException { if (!StringUtils.isNumeric(value)) { String[] arr = value.split(","); - for (String st : arr) { - if (!StringUtils.isNumeric(st)) { + for (String error : arr) { + if (!StringUtils.isNumeric(error)) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); } @@ -264,93 +255,87 @@ private void parameterIsNumber(String value) throws SQLServerException { } } - private void addElements(String[] s) throws SQLServerException { - if (s.length == 1) { - String errorWithoutOptionalPrefix = appendOrReplace(s[0]); - parameterIsNumber(errorWithoutOptionalPrefix); + private void addElements(String[] rule) throws SQLServerException { + if (rule.length == 1) { + String errorWithoutOptionalPrefix = appendOrReplace(rule[0]); + parameterIsNumeric(errorWithoutOptionalPrefix); isConnection = true; retryError = errorWithoutOptionalPrefix; - } else if (s.length == 2 || s.length == 3) { - parameterIsNumber(s[0]); - retryError = s[0]; - - String[] st = s[1].split(","); - parameterIsNumber(st[0]); - if (Integer.parseInt(st[0]) > 0) { - retryCount = Integer.parseInt(st[0]); + } else if (rule.length == 2 || rule.length == 3) { + parameterIsNumeric(rule[0]); + retryError = rule[0]; + String[] timings = rule[1].split(","); + parameterIsNumeric(timings[0]); + + if (Integer.parseInt(timings[0]) > 0) { + retryCount = Integer.parseInt(timings[0]); } else { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); } - if (st.length == 2) { - if (st[1].contains("*")) { - String[] sss = st[1].split("\\*"); - parameterIsNumber(sss[0]); - initialRetryTime = Integer.parseInt(sss[0]); + if (timings.length == 2) { + if (timings[1].contains("*")) { + String[] initialAndChange = timings[1].split("\\*"); + parameterIsNumeric(initialAndChange[0]); + initialRetryTime = Integer.parseInt(initialAndChange[0]); operand = "*"; - if (sss.length > 1) { - parameterIsNumber(sss[1]); - retryChange = Integer.parseInt(sss[1]); + if (initialAndChange.length > 1) { + parameterIsNumeric(initialAndChange[1]); + retryChange = Integer.parseInt(initialAndChange[1]); } else { retryChange = initialRetryTime; } - } else if (st[1].contains("+")) { - String[] sss = st[1].split("\\+"); - parameterIsNumber(sss[0]); + } else if (timings[1].contains("+")) { + String[] initialAndChange = timings[1].split("\\+"); + parameterIsNumeric(initialAndChange[0]); - initialRetryTime = Integer.parseInt(sss[0]); + initialRetryTime = Integer.parseInt(initialAndChange[0]); operand = "+"; - if (sss.length > 1) { - parameterIsNumber(sss[1]); - retryChange = Integer.parseInt(sss[1]); + if (initialAndChange.length > 1) { + parameterIsNumeric(initialAndChange[1]); + retryChange = Integer.parseInt(initialAndChange[1]); } } else { - parameterIsNumber(st[1]); - initialRetryTime = Integer.parseInt(st[1]); + parameterIsNumeric(timings[1]); + initialRetryTime = Integer.parseInt(timings[1]); } - } else if (st.length > 2) { - // If the timing options have more than 2 parts, they are badly formatted. + } else if (timings.length > 2) { StringBuilder builder = new StringBuilder(); - - for (String string : s) { + for (String string : rule) { builder.append(string); } - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); Object[] msgArgs = {builder.toString()}; throw new SQLServerException(null, form.format(msgArgs), null, 0, true); } - if (s.length == 3) { - retryQueries = (s[2].equals("0") ? "" : s[2].toLowerCase()); + if (rule.length == 3) { + retryQueries = (rule[2].equals("0") ? "" : rule[2].toLowerCase()); } } else { - // If the length is not 1,2,3, then the provided option is invalid StringBuilder builder = new StringBuilder(); - - for (String string : s) { + for (String string : rule) { builder.append(string); } - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); Object[] msgArgs = {builder.toString()}; throw new SQLServerException(null, form.format(msgArgs), null, 0, true); } } - private String appendOrReplace(String s) { - if (s.charAt(0) == '+') { + private String appendOrReplace(String retryError) { + if (retryError.charAt(0) == '+') { replaceExisting = false; - StringUtils.isNumeric(s.substring(1)); - return s.substring(1); + StringUtils.isNumeric(retryError.substring(1)); + return retryError.substring(1); } else { replaceExisting = true; - return s; + return retryError; } } - public void calcWaitTime() { + private void calcWaitTime() { for (int i = 0; i < retryCount; ++i) { int waitTime = initialRetryTime; if (operand.equals("+")) { @@ -361,7 +346,6 @@ public void calcWaitTime() { for (int k = 0; k < i; ++k) { waitTime *= retryChange; } - } waitTimes.add(waitTime); } From 632c168b57bb51578a535119e6eec4537742b9df Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 22 Apr 2024 10:50:02 -0700 Subject: [PATCH 10/57] Another missing null check --- .../jdbc/ConfigurableRetryLogic.java | 23 +++--- .../sqlserver/jdbc/SQLServerStatement.java | 76 ++++++++++--------- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 231b54bc7..597e7e391 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -15,9 +15,11 @@ public class ConfigurableRetryLogic { + private final static int INTERVAL_BETWEEN_READS = 30000; // How many ms must have elapsed before we re-read + private final static String DEFAULT_PROPS_FILE = "mssql-jdbc.properties"; + private static final java.util.logging.Logger CONFIGURABLE_RETRY_LOGGER = java.util.logging.Logger + .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); private static ConfigurableRetryLogic driverInstance = null; - private final static int intervalBetweenReads = 30000; // How many ms must have elapsed before we re-read - private final static String defaultPropsFile = "mssql-jdbc.properties"; private static long timeLastModified; private static long timeLastRead; private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) @@ -25,8 +27,7 @@ public class ConfigurableRetryLogic { private static boolean replaceFlag; // Are we replacing the list of transient errors (for connection retry)? private static HashMap cxnRules = new HashMap<>(); private static HashMap stmtRules = new HashMap<>(); - private static final java.util.logging.Logger configReadLogger = java.util.logging.Logger - .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); + private ConfigurableRetryLogic() throws SQLServerException { timeLastRead = new Date().getTime(); @@ -52,14 +53,14 @@ public static synchronized ConfigurableRetryLogic getInstance() throws SQLServer private static void reread() throws SQLServerException { long currentTime = new Date().getTime(); - if ((currentTime - timeLastRead) >= intervalBetweenReads && !compareModified()) { + if ((currentTime - timeLastRead) >= INTERVAL_BETWEEN_READS && !compareModified()) { timeLastRead = currentTime; setUpRules(); } } private static boolean compareModified() { - String inputToUse = getCurrentClassPath() + defaultPropsFile; + String inputToUse = getCurrentClassPath() + DEFAULT_PROPS_FILE; try { File f = new File(inputToUse); @@ -145,8 +146,8 @@ private static String getCurrentClassPath() { URI uri = new URI(location + "/"); return uri.getPath(); } catch (Exception e) { - if (configReadLogger.isLoggable(java.util.logging.Level.FINEST)) { - configReadLogger.finest("Unable to get current class path for properties file reading."); + if (CONFIGURABLE_RETRY_LOGGER.isLoggable(java.util.logging.Level.FINEST)) { + CONFIGURABLE_RETRY_LOGGER.finest("Unable to get current class path for properties file reading."); } } return null; @@ -157,7 +158,7 @@ private static LinkedList readFromFile() { LinkedList list = new LinkedList<>(); try { - File f = new File(filePath + defaultPropsFile); + File f = new File(filePath + DEFAULT_PROPS_FILE); timeLastModified = f.lastModified(); try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { String readLine; @@ -169,8 +170,8 @@ private static LinkedList readFromFile() { } } } catch (IOException e) { - if (configReadLogger.isLoggable(java.util.logging.Level.FINEST)) { - configReadLogger.finest("No properties file exists or file is badly formatted."); + if (CONFIGURABLE_RETRY_LOGGER.isLoggable(java.util.logging.Level.FINEST)) { + CONFIGURABLE_RETRY_LOGGER.finest("No properties file exists or file is badly formatted."); } } return list; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 24b07a344..67bfa582c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -268,48 +268,50 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ // (Re)execute this Statement with the new command executeCommand(newStmtCmd); } catch (SQLServerException e) { - ConfigRetryRule rule = ConfigurableRetryLogic.getInstance() - .searchRuleSet(e.getSQLServerError().getErrorNumber(), "statement"); - boolean meetsQueryMatch = true; - - if (rule != null && retryAttempt < rule.getRetryCount()) { - if (!(rule.getRetryQueries().isEmpty())) { - // If query has been defined for the rule, we need to query match - meetsQueryMatch = rule.getRetryQueries() - .contains(ConfigurableRetryLogic.getInstance().getLastQuery().split(" ")[0]); - } + SQLServerError sqlServerError = e.getSQLServerError(); - if (meetsQueryMatch) { - try { - int timeToWait = rule.getWaitTimes().get(retryAttempt); - if (connection.getQueryTimeoutSeconds() >= 0 - && timeToWait > connection.getQueryTimeoutSeconds()) { - MessageFormat form = new MessageFormat( - SQLServerException.getErrString("R_InvalidRetryInterval")); - throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); - } + if (null == sqlServerError) { + throw e; + } else { + + ConfigRetryRule rule = ConfigurableRetryLogic.getInstance().searchRuleSet(e.getSQLServerError().getErrorNumber(), "statement"); + boolean meetsQueryMatch = true; + + if (rule != null && retryAttempt < rule.getRetryCount()) { + if (!(rule.getRetryQueries().isEmpty())) { + // If query has been defined for the rule, we need to query match + meetsQueryMatch = rule.getRetryQueries().contains(ConfigurableRetryLogic.getInstance().getLastQuery().split(" ")[0]); + } + + if (meetsQueryMatch) { try { - Thread.sleep(TimeUnit.SECONDS.toMillis(timeToWait)); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } catch (IndexOutOfBoundsException exc) { - MessageFormat form = new MessageFormat( - SQLServerException.getErrString("R_indexOutOfRange")); - Object[] msgArgs = {retryAttempt}; - throw new SQLServerException(this, form.format(msgArgs), null, 0, false); + int timeToWait = rule.getWaitTimes().get(retryAttempt); + if (connection.getQueryTimeoutSeconds() >= 0 && timeToWait > connection.getQueryTimeoutSeconds()) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRetryInterval")); + throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); + } + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(timeToWait)); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } catch (IndexOutOfBoundsException exc) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_indexOutOfRange")); + Object[] msgArgs = {retryAttempt}; + throw new SQLServerException(this, form.format(msgArgs), null, 0, false); + } + cont = true; + retryAttempt++; } - cont = true; - retryAttempt++; - } - } else if (e.getDriverErrorCode() == SQLServerException.ERROR_QUERY_TIMEOUT) { - if (e.getCause() == null) { - throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e); + } else if (e.getDriverErrorCode() == SQLServerException.ERROR_QUERY_TIMEOUT) { + if (e.getCause() == null) { + throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e); + } + throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e.getCause()); + } else { + throw e; } - throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e.getCause()); - } else { - throw e; } } finally { if (newStmtCmd.wasExecuted()) { From be5574923571d268840edb752a8a3cb65d0f7878 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 23 Apr 2024 09:44:11 -0700 Subject: [PATCH 11/57] Fix for timeout tests --- .../sqlserver/jdbc/SQLServerStatement.java | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 67bfa582c..466717db1 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -269,49 +269,47 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ executeCommand(newStmtCmd); } catch (SQLServerException e) { SQLServerError sqlServerError = e.getSQLServerError(); + ConfigRetryRule rule = null; - if (null == sqlServerError) { - throw e; - } else { + if (null != sqlServerError) { + rule = ConfigurableRetryLogic.getInstance().searchRuleSet(e.getSQLServerError().getErrorNumber(), "statement"); + } - ConfigRetryRule rule = ConfigurableRetryLogic.getInstance().searchRuleSet(e.getSQLServerError().getErrorNumber(), "statement"); + if (null != rule && retryAttempt < rule.getRetryCount()) { boolean meetsQueryMatch = true; + if (!(rule.getRetryQueries().isEmpty())) { + // If query has been defined for the rule, we need to query match + meetsQueryMatch = rule.getRetryQueries().contains(ConfigurableRetryLogic.getInstance().getLastQuery().split(" ")[0]); + } - if (rule != null && retryAttempt < rule.getRetryCount()) { - if (!(rule.getRetryQueries().isEmpty())) { - // If query has been defined for the rule, we need to query match - meetsQueryMatch = rule.getRetryQueries().contains(ConfigurableRetryLogic.getInstance().getLastQuery().split(" ")[0]); - } - - if (meetsQueryMatch) { + if (meetsQueryMatch) { + try { + int timeToWait = rule.getWaitTimes().get(retryAttempt); + if (connection.getQueryTimeoutSeconds() >= 0 && timeToWait > connection.getQueryTimeoutSeconds()) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRetryInterval")); + throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); + } try { - int timeToWait = rule.getWaitTimes().get(retryAttempt); - if (connection.getQueryTimeoutSeconds() >= 0 && timeToWait > connection.getQueryTimeoutSeconds()) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRetryInterval")); - throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); - } - try { - Thread.sleep(TimeUnit.SECONDS.toMillis(timeToWait)); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } catch (IndexOutOfBoundsException exc) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_indexOutOfRange")); - Object[] msgArgs = {retryAttempt}; - throw new SQLServerException(this, form.format(msgArgs), null, 0, false); - + Thread.sleep(TimeUnit.SECONDS.toMillis(timeToWait)); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); } - cont = true; - retryAttempt++; - } - } else if (e.getDriverErrorCode() == SQLServerException.ERROR_QUERY_TIMEOUT) { - if (e.getCause() == null) { - throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e); + } catch (IndexOutOfBoundsException exc) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_indexOutOfRange")); + Object[] msgArgs = {retryAttempt}; + throw new SQLServerException(this, form.format(msgArgs), null, 0, false); + } - throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e.getCause()); - } else { - throw e; + cont = true; + retryAttempt++; } + } else if (e.getDriverErrorCode() == SQLServerException.ERROR_QUERY_TIMEOUT) { + if (e.getCause() == null) { + throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e); + } + throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e.getCause()); + } else { + throw e; } } finally { if (newStmtCmd.wasExecuted()) { From 68c683a17707e88f685a4dc7a25eafa8d8ea7deb Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 23 Apr 2024 10:23:16 -0700 Subject: [PATCH 12/57] Added timing tests + test comments --- .../jdbc/ConfigurableRetryLogicTest.java | 145 ++++++++++++++++-- 1 file changed, 133 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index dd5257612..9f05316b9 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -9,6 +9,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -26,6 +27,16 @@ public class ConfigurableRetryLogicTest extends AbstractTest { private static final String tableName = AbstractSQLGenerator .escapeIdentifier(RandomUtil.getIdentifier("crlTestTable")); + @BeforeAll + public static void setupTests() throws Exception { + setConnection(); + } + + /** + * Tests that statement retry works with prepared statements. + * @throws Exception + * if unable to connect or execute against db + */ @Test public void testStatementRetryPreparedStatement() throws Exception { connectionStringCRL = TestUtils.addOrOverrideProperty(connectionString, "retryExec", @@ -46,6 +57,11 @@ public void testStatementRetryPreparedStatement() throws Exception { } } + /** + * Tests that statement retry works with callable statements. + * @throws Exception + * if unable to connect or execute against db + */ @Test public void testStatementRetryCallableStatement() throws Exception { connectionStringCRL = TestUtils.addOrOverrideProperty(connectionString, "retryExec", @@ -67,6 +83,11 @@ public void testStatementRetryCallableStatement() throws Exception { } } + /** + * Tests that statement retry works with statements. Used in below negative testing. + * @throws Exception + * if unable to connect or execute against db + */ public void testStatementRetry(String addedRetryParams) throws Exception { String cxnString = connectionString + addedRetryParams; // 2714 There is already an object named x @@ -84,6 +105,11 @@ public void testStatementRetry(String addedRetryParams) throws Exception { } } + /** + * Tests that statement retry works with statements. A different error is expected here than the test above. + * @throws Exception + * if unable to connect or execute against db + */ public void testStatementRetryWithShortQueryTimeout(String addedRetryParams) throws Exception { String cxnString = connectionString + addedRetryParams; // 2714 There is already an object named x @@ -98,11 +124,83 @@ public void testStatementRetryWithShortQueryTimeout(String addedRetryParams) thr } } + /** + * Tests that the correct number of retries are happening for all statement scenarios. Tests are expected to take + * a minimum of the sum of whatever has been defined for the waiting intervals, and maximum of the previous sum + * plus some amount of time to account for test environment slowness. + */ @Test - public void timingTests() { + public void statementTimingTests() { + long totalTime; + long timerStart = System.currentTimeMillis(); + long expectedTime = 5; + + // A single retry immediately + try { + testStatementRetry("retryExec={2714:1;};"); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedTime), + "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedTime)); + } + + timerStart = System.currentTimeMillis(); + + // A single retry waiting 5 seconds + try { + testStatementRetry("retryExec={2714:1,5;};"); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(5), + "total time: " + totalTime + ", expected minimum time: " + + TimeUnit.SECONDS.toMillis(5)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(5 + expectedTime), + "total time: " + totalTime + ", expected maximum time: " + + TimeUnit.SECONDS.toMillis(5 + expectedTime)); + } + timerStart = System.currentTimeMillis(); + + // Two retries. The first after 2 seconds, the next after 6 + try { + testStatementRetry("retryExec={2714,2716:2,2*3:CREATE};"); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(8), + "total time: " + totalTime + ", expected minimum time: " + + TimeUnit.SECONDS.toMillis(8)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(8 + expectedTime), + "total time: " + totalTime + ", expected maximum time: " + + TimeUnit.SECONDS.toMillis(8 + expectedTime)); + } + + timerStart = System.currentTimeMillis(); + + // Two retries. The first after 3 seconds, the next after 7 + try { + testStatementRetry("retryExec={2714,2716:2,3+4:CREATE};"); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(10), + "total time: " + totalTime + ", expected minimum time: " + + TimeUnit.SECONDS.toMillis(10)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(10 + expectedTime), + "total time: " + totalTime + ", expected maximum time: " + + TimeUnit.SECONDS.toMillis(10 + expectedTime)); + } } + /** + * Tests that CRL is able to read from a properties file, in the event the connection property is not used. + */ @Test public void readFromFile() { try { @@ -112,8 +210,11 @@ public void readFromFile() { } } + /** + * Tests that rules of the correct length, and containing valid values, pass + */ @Test - public void testCorrectlyFormattedRulesPass() { + public void testCorrectlyFormattedRules() { // Correctly formatted rules try { // Empty rule set @@ -148,8 +249,13 @@ public void testCorrectlyFormattedRulesPass() { } } + /** + * Tests that rules with an invalid retry error correctly fail. + * @throws Exception + * for the invalid parameter + */ @Test - public void testCorrectRetryError() throws Exception { + public void testRetryError() throws Exception { // Test incorrect format (NaN) try { testStatementRetry("retryExec={TEST};"); @@ -165,8 +271,13 @@ public void testCorrectRetryError() throws Exception { } } + /** + * Tests that rules with an invalid retry count correctly fail. + * @throws Exception + * for the invalid parameter + */ @Test - public void testProperRetryCount() throws Exception { + public void testRetryCount() throws Exception { // Test min try { testStatementRetry("retryExec={2714,2716:-1,2+2:CREATE};"); @@ -182,8 +293,13 @@ public void testProperRetryCount() throws Exception { } } + /** + * Tests that rules with an invalid initial retry time correctly fail. + * @throws Exception + * for the invalid parameter + */ @Test - public void testProperInitialRetryTime() throws Exception { + public void testInitialRetryTime() throws Exception { // Test min try { testStatementRetry("retryExec={2714,2716:4,-1+1:CREATE};"); @@ -199,8 +315,13 @@ public void testProperInitialRetryTime() throws Exception { } } + /** + * Tests that rules with an invalid operand correctly fail. + * @throws Exception + * for the invalid parameter + */ @Test - public void testProperOperand() throws Exception { + public void testOperand() throws Exception { // Test incorrect try { testStatementRetry("retryExec={2714,2716:1,2AND2:CREATE};"); @@ -209,8 +330,13 @@ public void testProperOperand() throws Exception { } } + /** + * Tests that rules with an invalid retry change correctly fail. + * @throws Exception + * for the invalid parameter + */ @Test - public void testProperRetryChange() throws Exception { + public void testRetryChange() throws Exception { // Test incorrect try { testStatementRetry("retryExec={2714,2716:1,2+2:CREATE};"); @@ -227,9 +353,4 @@ private static void createTable(Statement stmt) throws SQLException { private static void dropTable(Statement stmt) throws SQLException { TestUtils.dropTableIfExists(tableName, stmt); } - - @BeforeAll - public static void setupTests() throws Exception { - setConnection(); - } } From 9730690c3690964180d1de76831d1030981e6698 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 23 Apr 2024 10:27:07 -0700 Subject: [PATCH 13/57] Formatting --- .../jdbc/ConfigurableRetryLogic.java | 1 - .../sqlserver/jdbc/SQLServerDataSource.java | 6 +-- .../sqlserver/jdbc/SQLServerDriver.java | 2 +- .../sqlserver/jdbc/SQLServerStatement.java | 15 ++++-- .../jdbc/ConfigurableRetryLogicTest.java | 51 ++++++++++--------- .../jdbc/SQLServerConnectionTest.java | 5 +- 6 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 597e7e391..8d31cfc0a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -28,7 +28,6 @@ public class ConfigurableRetryLogic { private static HashMap cxnRules = new HashMap<>(); private static HashMap stmtRules = new HashMap<>(); - private ConfigurableRetryLogic() throws SQLServerException { timeLastRead = new Date().getTime(); setUpRules(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index 7a469adae..8de0b137c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -1366,14 +1366,12 @@ public boolean getCalcBigDecimalPrecision() { @Override public void setRetryExec(String retryExec) { - setStringProperty(connectionProps, SQLServerDriverStringProperty.RETRY_EXEC.toString(), - retryExec); + setStringProperty(connectionProps, SQLServerDriverStringProperty.RETRY_EXEC.toString(), retryExec); } @Override public String getRetryExec() { - return getStringProperty(connectionProps, SQLServerDriverStringProperty.RETRY_EXEC.toString(), - null); + return getStringProperty(connectionProps, SQLServerDriverStringProperty.RETRY_EXEC.toString(), null); } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 69398436b..cee77e126 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -611,7 +611,7 @@ enum SQLServerDriverStringProperty { SERVER_CERTIFICATE("serverCertificate", ""), DATETIME_DATATYPE("datetimeParameterType", DatetimeType.DATETIME2.toString()), ACCESS_TOKEN_CALLBACK_CLASS("accessTokenCallbackClass", ""), - RETRY_EXEC("retryExec",""); + RETRY_EXEC("retryExec", ""); private final String name; private final String defaultValue; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 466717db1..6f309b5c1 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -272,21 +272,25 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ ConfigRetryRule rule = null; if (null != sqlServerError) { - rule = ConfigurableRetryLogic.getInstance().searchRuleSet(e.getSQLServerError().getErrorNumber(), "statement"); + rule = ConfigurableRetryLogic.getInstance().searchRuleSet(e.getSQLServerError().getErrorNumber(), + "statement"); } if (null != rule && retryAttempt < rule.getRetryCount()) { boolean meetsQueryMatch = true; if (!(rule.getRetryQueries().isEmpty())) { // If query has been defined for the rule, we need to query match - meetsQueryMatch = rule.getRetryQueries().contains(ConfigurableRetryLogic.getInstance().getLastQuery().split(" ")[0]); + meetsQueryMatch = rule.getRetryQueries() + .contains(ConfigurableRetryLogic.getInstance().getLastQuery().split(" ")[0]); } if (meetsQueryMatch) { try { int timeToWait = rule.getWaitTimes().get(retryAttempt); - if (connection.getQueryTimeoutSeconds() >= 0 && timeToWait > connection.getQueryTimeoutSeconds()) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRetryInterval")); + if (connection.getQueryTimeoutSeconds() >= 0 + && timeToWait > connection.getQueryTimeoutSeconds()) { + MessageFormat form = new MessageFormat( + SQLServerException.getErrString("R_InvalidRetryInterval")); throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); } try { @@ -295,7 +299,8 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ Thread.currentThread().interrupt(); } } catch (IndexOutOfBoundsException exc) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_indexOutOfRange")); + MessageFormat form = new MessageFormat( + SQLServerException.getErrString("R_indexOutOfRange")); Object[] msgArgs = {retryAttempt}; throw new SQLServerException(this, form.format(msgArgs), null, 0, false); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 9f05316b9..41ee28fa0 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -34,8 +34,9 @@ public static void setupTests() throws Exception { /** * Tests that statement retry works with prepared statements. + * * @throws Exception - * if unable to connect or execute against db + * if unable to connect or execute against db */ @Test public void testStatementRetryPreparedStatement() throws Exception { @@ -59,8 +60,9 @@ public void testStatementRetryPreparedStatement() throws Exception { /** * Tests that statement retry works with callable statements. + * * @throws Exception - * if unable to connect or execute against db + * if unable to connect or execute against db */ @Test public void testStatementRetryCallableStatement() throws Exception { @@ -85,8 +87,9 @@ public void testStatementRetryCallableStatement() throws Exception { /** * Tests that statement retry works with statements. Used in below negative testing. + * * @throws Exception - * if unable to connect or execute against db + * if unable to connect or execute against db */ public void testStatementRetry(String addedRetryParams) throws Exception { String cxnString = connectionString + addedRetryParams; // 2714 There is already an object named x @@ -107,8 +110,9 @@ public void testStatementRetry(String addedRetryParams) throws Exception { /** * Tests that statement retry works with statements. A different error is expected here than the test above. + * * @throws Exception - * if unable to connect or execute against db + * if unable to connect or execute against db */ public void testStatementRetryWithShortQueryTimeout(String addedRetryParams) throws Exception { String cxnString = connectionString + addedRetryParams; // 2714 There is already an object named x @@ -156,11 +160,9 @@ public void statementTimingTests() { } finally { totalTime = System.currentTimeMillis() - timerStart; assertTrue(totalTime > TimeUnit.SECONDS.toMillis(5), - "total time: " + totalTime + ", expected minimum time: " - + TimeUnit.SECONDS.toMillis(5)); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(5 + expectedTime), - "total time: " + totalTime + ", expected maximum time: " - + TimeUnit.SECONDS.toMillis(5 + expectedTime)); + "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(5)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(5 + expectedTime), "total time: " + totalTime + + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(5 + expectedTime)); } timerStart = System.currentTimeMillis(); @@ -173,11 +175,9 @@ public void statementTimingTests() { } finally { totalTime = System.currentTimeMillis() - timerStart; assertTrue(totalTime > TimeUnit.SECONDS.toMillis(8), - "total time: " + totalTime + ", expected minimum time: " - + TimeUnit.SECONDS.toMillis(8)); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(8 + expectedTime), - "total time: " + totalTime + ", expected maximum time: " - + TimeUnit.SECONDS.toMillis(8 + expectedTime)); + "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(8)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(8 + expectedTime), "total time: " + totalTime + + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(8 + expectedTime)); } timerStart = System.currentTimeMillis(); @@ -190,11 +190,9 @@ public void statementTimingTests() { } finally { totalTime = System.currentTimeMillis() - timerStart; assertTrue(totalTime > TimeUnit.SECONDS.toMillis(10), - "total time: " + totalTime + ", expected minimum time: " - + TimeUnit.SECONDS.toMillis(10)); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(10 + expectedTime), - "total time: " + totalTime + ", expected maximum time: " - + TimeUnit.SECONDS.toMillis(10 + expectedTime)); + "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(10)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(10 + expectedTime), "total time: " + totalTime + + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(10 + expectedTime)); } } @@ -251,8 +249,9 @@ public void testCorrectlyFormattedRules() { /** * Tests that rules with an invalid retry error correctly fail. + * * @throws Exception - * for the invalid parameter + * for the invalid parameter */ @Test public void testRetryError() throws Exception { @@ -273,8 +272,9 @@ public void testRetryError() throws Exception { /** * Tests that rules with an invalid retry count correctly fail. + * * @throws Exception - * for the invalid parameter + * for the invalid parameter */ @Test public void testRetryCount() throws Exception { @@ -295,8 +295,9 @@ public void testRetryCount() throws Exception { /** * Tests that rules with an invalid initial retry time correctly fail. + * * @throws Exception - * for the invalid parameter + * for the invalid parameter */ @Test public void testInitialRetryTime() throws Exception { @@ -317,8 +318,9 @@ public void testInitialRetryTime() throws Exception { /** * Tests that rules with an invalid operand correctly fail. + * * @throws Exception - * for the invalid parameter + * for the invalid parameter */ @Test public void testOperand() throws Exception { @@ -332,8 +334,9 @@ public void testOperand() throws Exception { /** * Tests that rules with an invalid retry change correctly fail. + * * @throws Exception - * for the invalid parameter + * for the invalid parameter */ @Test public void testRetryChange() throws Exception { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java index bf2aabad1..e5fdc7e35 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java @@ -5,9 +5,9 @@ package com.microsoft.sqlserver.jdbc; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; import java.io.Reader; @@ -213,8 +213,7 @@ public void testDataSource() throws SQLServerException { assertEquals(booleanPropValue, ds.getCalcBigDecimalPrecision(), TestResource.getResource("R_valuesAreDifferent")); ds.setRetryExec(stringPropValue); - assertEquals(stringPropValue, ds.getRetryExec(), - TestResource.getResource("R_valuesAreDifferent")); + assertEquals(stringPropValue, ds.getRetryExec(), TestResource.getResource("R_valuesAreDifferent")); ds.setServerCertificate(stringPropValue); assertEquals(stringPropValue, ds.getServerCertificate(), TestResource.getResource("R_valuesAreDifferent")); From d9d9b384a1832f7be68b52791e7389b0da42c6e7 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 23 Apr 2024 13:54:21 -0700 Subject: [PATCH 14/57] Added a multiple rules test --- .../sqlserver/jdbc/ConfigurableRetryLogicTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 41ee28fa0..f2b03422a 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -196,6 +196,18 @@ public void statementTimingTests() { } } + /** + * Tests that CRL works with multiple rules provided at once. + */ + @Test + public void multipleRules() { + try { + testStatementRetry("retryExec={2716:1,2*2:CREATE;2714:1,2*2:CREATE};"); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } + } + /** * Tests that CRL is able to read from a properties file, in the event the connection property is not used. */ From 07fd965e0ecbc9aedb4a9512e3988490643e598e Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 15 Apr 2024 10:19:23 -0700 Subject: [PATCH 15/57] Trying on a new branch b/c I keep getting build failures and I have no idea why... --- .../microsoft/sqlserver/jdbc/ConfigRead.java | 217 ++++++++++++++++++ .../sqlserver/jdbc/ConfigRetryRule.java | 183 +++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java new file mode 100644 index 000000000..db6a39e03 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java @@ -0,0 +1,217 @@ +package com.microsoft.sqlserver.jdbc; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URI; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + + +public class ConfigRead { + private final static int intervalBetweenReads = 30000; // How many ms must have elapsed before we re-read + private final static String defaultPropsFile = "mssql-jdbc.properties"; + private static ConfigRead driverInstance = null; + private static long timeLastModified; + private static long lastTimeRead; + private static String lastQuery = ""; + private static String customRetryRules = ""; // Rules imported from connection string + private static boolean replaceFlag; // Are we replacing the list of transient errors (for connection retry)? + private static HashMap cxnRules = new HashMap<>(); + private static HashMap stmtRules = new HashMap<>(); + + private ConfigRead() throws SQLServerException { + // On instantiation, set last time read and set up rules + lastTimeRead = new Date().getTime(); + setUpRules(); + } + + /** + * Fetches the static instance of ConfigRead, instantiating it if it hasn't already been. + * + * @return The static instance of ConfigRead + * @throws SQLServerException + * an exception + */ + public static synchronized ConfigRead getInstance() throws SQLServerException { + // Every time we fetch this static instance, instantiate if it hasn't been. If it has then re-read and return + // the instance. + if (driverInstance == null) { + driverInstance = new ConfigRead(); + } else { + reread(); + } + + return driverInstance; + } + + /** + * Check if it's time to re-read, and if the file has changed. If so, then re-set up rules. + * + * @throws SQLServerException + * an exception + */ + private static void reread() throws SQLServerException { + long currentTime = new Date().getTime(); + + if ((currentTime - lastTimeRead) >= intervalBetweenReads && !compareModified()) { + lastTimeRead = currentTime; + setUpRules(); + } + } + + private static boolean compareModified() { + String inputToUse = getCurrentClassPath() + defaultPropsFile; + + try { + File f = new File(inputToUse); + return f.lastModified() == timeLastModified; + } catch (Exception e) { + return false; + } + } + + public void setCustomRetryRules(String cRR) throws SQLServerException { + customRetryRules = cRR; + setUpRules(); + } + + public void setFromConnectionString(String custom) throws SQLServerException { + if (!custom.isEmpty()) { + setCustomRetryRules(custom); + } + } + + public void storeLastQuery(String sql) { + lastQuery = sql.toLowerCase(); + } + + public String getLastQuery() { + return lastQuery; + } + + private static void setUpRules() throws SQLServerException { + LinkedList temp = null; + + if (!customRetryRules.isEmpty()) { + // If user as set custom rules in connection string, then we use those over any file + temp = new LinkedList<>(); + for (String s : customRetryRules.split(";")) { + temp.add(s); + } + } else { + try { + temp = readFromFile(); + } catch (IOException e) { + // TODO handle IO exception + } + } + + if (temp != null) { + createRules(temp); + } + } + + private static void createRules(LinkedList list) throws SQLServerException { + cxnRules = new HashMap<>(); + stmtRules = new HashMap<>(); + + for (String temp : list) { + + ConfigRetryRule rule = new ConfigRetryRule(temp); + if (rule.getError().contains(",")) { + + String[] arr = rule.getError().split(","); + + for (String s : arr) { + ConfigRetryRule rulez = new ConfigRetryRule(s, rule); + if (rule.getConnectionStatus()) { + if (rule.getReplaceExisting()) { + cxnRules = new HashMap<>(); + replaceFlag = true; + } + cxnRules.put(Integer.parseInt(rulez.getError()), rulez); + } else { + stmtRules.put(Integer.parseInt(rulez.getError()), rulez); + } + } + } else { + if (rule.getConnectionStatus()) { + if (rule.getReplaceExisting()) { + cxnRules = new HashMap<>(); + replaceFlag = true; + } + cxnRules.put(Integer.parseInt(rule.getError()), rule); + } else { + stmtRules.put(Integer.parseInt(rule.getError()), rule); + } + } + } + } + + private static String getCurrentClassPath() { + try { + String className = new Object() {}.getClass().getEnclosingClass().getName(); + String location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath(); + location = location.substring(0, location.length() - 16); + URI uri = new URI(location + "/"); + return uri.getPath(); + } catch (Exception e) { + // TODO handle exception + } + return null; + } + + private static LinkedList readFromFile() throws IOException { + String filePath = getCurrentClassPath(); + + LinkedList list = new LinkedList<>(); + try { + File f = new File(filePath + defaultPropsFile); + timeLastModified = f.lastModified(); + try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { + String readLine; + + while ((readLine = buffer.readLine()) != null) { + if (readLine.startsWith("retryExec")) { + String value = readLine.split("=")[1]; + for (String s : value.split(";")) { + list.add(s); + } + } + // list.add(readLine); + } + } + } catch (IOException e) { + // TODO handle IO Exception + throw new IOException(); + } + return list; + } + + public ConfigRetryRule searchRuleSet(int ruleToSearch, String ruleSet) throws SQLServerException { + reread(); + if (ruleSet.equals("statement")) { + for (Map.Entry entry : stmtRules.entrySet()) { + if (entry.getKey() == ruleToSearch) { + return entry.getValue(); + } + } + } else { + for (Map.Entry entry : cxnRules.entrySet()) { + if (entry.getKey() == ruleToSearch) { + return entry.getValue(); + } + } + } + + return null; + } + + public boolean getReplaceFlag() { + return replaceFlag; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java new file mode 100644 index 000000000..cc9adcc9e --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java @@ -0,0 +1,183 @@ +package com.microsoft.sqlserver.jdbc; + +import java.text.MessageFormat; +import java.util.ArrayList; + + +public class ConfigRetryRule { + private String retryError; + private String operand = "*"; + private int initialRetryTime = 10; + private int retryChange = 2; + private int retryCount; + private String retryQueries = ""; + private ArrayList waitTimes = new ArrayList<>(); + private boolean isConnection = false; + private boolean replaceExisting = false; + + public ConfigRetryRule(String s) throws SQLServerException { + String[] stArr = parse(s); + addElements(stArr); + calcWaitTime(); + } + + public ConfigRetryRule(String rule, ConfigRetryRule r) { + copyFromCopy(r); + this.retryError = rule; + } + + private void copyFromCopy(ConfigRetryRule r) { + this.retryError = r.getError(); + this.operand = r.getOperand(); + this.initialRetryTime = r.getInitialRetryTime(); + this.retryChange = r.getRetryChange(); + this.retryCount = r.getRetryCount(); + this.retryQueries = r.getRetryQueries(); + this.waitTimes = r.getWaitTimes(); + this.isConnection = r.getConnectionStatus(); + } + + private String[] parse(String s) { + String temp = s + " "; + + temp = temp.replace(": ", ":0"); + temp = temp.replace("{", ""); + temp = temp.replace("}", ""); + temp = temp.trim(); + + // We want to do an empty string check here + + if (temp.isEmpty()) { + + } + + return temp.split(":"); + } + + private void addElements(String[] s) throws SQLServerException { + // +"retryExec={2714,2716:1,2*2:CREATE;2715:1,3;+4060,4070};" + if (s.length == 1) { + // If single element, connection + isConnection = true; + retryError = appendOrReplace(s[0]); + } else if (s.length == 2 || s.length == 3) { + // If 2 or 3, statement, either with or without query + // Parse first element (statement rules) + if (!StringUtils.isNumeric(s[0])) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); + Object[] msgArgs = {s[0], "\"Retry Error\""}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } else { + retryError = s[0]; + } + + // Parse second element (retry options) + String[] st = s[1].split(","); + + // We have retry count AND timing rules + retryCount = Integer.parseInt(st[0]); // TODO input validation + // Second half can either be N, N OP, N OP N + if (st[1].contains("*")) { + // We know its either N OP, N OP N + String[] sss = st[1].split("/*"); + initialRetryTime = Integer.parseInt(sss[0]); + operand = "*"; + if (sss.length > 2) { + retryChange = Integer.parseInt(sss[2]); + } + } else if (st[1].contains("+")) { + // We know its either N OP, N OP N + String[] sss = st[1].split("/+"); + initialRetryTime = Integer.parseInt(sss[0]); + operand = "*"; + if (sss.length > 2) { + retryChange = Integer.parseInt(sss[2]); + } + } else { + initialRetryTime = Integer.parseInt(st[1]); + // TODO set defaults + } + if (s.length == 3) { + // Query has also been provided + retryQueries = (s[2].equals("0") ? "" : s[2].toLowerCase()); + } + } else { + // If the length is not 1,2,3, then the provided option is invalid + // Prov + + StringBuilder builder = new StringBuilder(); + + for (String string : s) { + builder.append(string); + } + + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); + Object[] msgArgs = {builder.toString()}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + } + + private String appendOrReplace(String s) { + if (s.charAt(0) == '+') { + replaceExisting = false; + StringUtils.isNumeric(s.substring(1)); + return s.substring(1); + } else { + replaceExisting = true; + return s; + } + } + + public void calcWaitTime() { + for (int i = 0; i < retryCount; ++i) { + int waitTime = initialRetryTime; + if (operand.equals("+")) { + for (int j = 0; j < i; ++j) { + waitTime += retryChange; + } + } else if (operand.equals("*")) { + for (int k = 0; k < i; ++k) { + waitTime *= retryChange; + } + + } + waitTimes.add(waitTime); + } + } + + public String getError() { + return retryError; + } + + public String getOperand() { + return operand; + } + + public int getInitialRetryTime() { + return initialRetryTime; + } + + public int getRetryChange() { + return retryChange; + } + + public int getRetryCount() { + return retryCount; + } + + public boolean getConnectionStatus() { + return isConnection; + } + + public String getRetryQueries() { + return retryQueries; + } + + public ArrayList getWaitTimes() { + return waitTimes; + } + + public boolean getReplaceExisting() { + return replaceExisting; + } +} From 03aa1b742eb55f0bda9d7ea21c7f5a8e59870aa9 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 7 Jun 2024 10:33:27 -0700 Subject: [PATCH 16/57] More changes --- .../jdbc/ConfigurableRetryLogic.java | 2 +- .../sqlserver/jdbc/SQLServerConnection.java | 11 ++- .../jdbc/ConfigurableRetryLogicTest.java | 75 +++++++++++++++++++ .../jdbc/unit/statement/LimitEscapeTest.java | 10 +-- 4 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 8d31cfc0a..872e56e9c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -24,7 +24,7 @@ public class ConfigurableRetryLogic { private static long timeLastRead; private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) private static String rulesFromConnectionString = ""; - private static boolean replaceFlag; // Are we replacing the list of transient errors (for connection retry)? + private static boolean replaceFlag = false; // Are we replacing the list of transient errors (for connection retry)? private static HashMap cxnRules = new HashMap<>(); private static HashMap stmtRules = new HashMap<>(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index b6be87c70..a578aeb11 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -2023,9 +2023,14 @@ Connection connect(Properties propsIn, SQLServerPooledConnection pooledConnectio ConfigRetryRule rule = ConfigurableRetryLogic.getInstance() .searchRuleSet(sqlServerError.getErrorNumber(), "connection"); - if (null == rule && !ConfigurableRetryLogic.getInstance().getReplaceFlag() - && !TransientError.isTransientError(sqlServerError)) { - throw e; + if (null == rule) { + if (ConfigurableRetryLogic.getInstance().getReplaceFlag()) { + throw e; + } else { + if (!TransientError.isTransientError(sqlServerError)) { + throw e; + } + } } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index f2b03422a..b2181695d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -108,6 +108,25 @@ public void testStatementRetry(String addedRetryParams) throws Exception { } } + public void testConnectionRetry(String replacedDbName, String addedRetryParams) throws Exception { + String cxnString = connectionString + addedRetryParams; // 2714 There is already an object named x + cxnString = TestUtils.addOrOverrideProperty(cxnString, "database", replacedDbName); + + try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { + try { + //createTable(s); + //s.execute("create table " + tableName + " (c1 int null);"); + fail(TestResource.getResource("R_expectedFailPassed")); + } catch (Exception e) { + System.out.println("blah"); + assertTrue(e.getMessage().startsWith("There is already an object"), + TestResource.getResource("R_unexpectedExceptionContent") + ": " + e.getMessage()); + } finally { + dropTable(s); + } + } + } + /** * Tests that statement retry works with statements. A different error is expected here than the test above. * @@ -360,6 +379,62 @@ public void testRetryChange() throws Exception { } } + @Test + public void testConnectionRetry() throws Exception { + // Test retry with a single connection rule (replace) + + // Replace existing rules with our own + + try { + //testConnectionRetry("blah","retryExec={4060};"); + testConnectionRetry("blah","retryExec={9999};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); + } + + // Test retry with a single connection rule (append) + + // Test retry with multiple connection rules +// try { +// testStatementRetry("retryExec={:1,2*2:CREATE};"); +// } catch (SQLServerException e) { +// assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); +// } + } + + @Test + public void connectionTimingTest() throws Exception { + long totalTime; + long timerStart = System.currentTimeMillis(); + long expectedTime = 5; + + // No retries since CRL rules override + try { + testConnectionRetry("blah","retryExec={9999};"); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + System.out.println("totalTime: " + totalTime); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedTime), + "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedTime)); + } + + timerStart = System.currentTimeMillis(); + + // Now retry, timing should reflect this + try { + testConnectionRetry("blah","retryExec={4060};"); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + System.out.println("totalTime: " + totalTime); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedTime), + "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedTime)); + } + } + private static void createTable(Statement stmt) throws SQLException { String sql = "create table " + tableName + " (c1 int null);"; stmt.execute(sql); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java index e54b6760f..b52dd4a69 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java @@ -46,11 +46,11 @@ public class LimitEscapeTest extends AbstractTest { private static Vector offsetQuery = new Vector<>(); // TODO: remove quote for now to avoid bug in driver - static String table1 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t1").replaceAll("\'", ""); - static String table2 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t2").replaceAll("\'", ""); - static String table3 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t3").replaceAll("\'", ""); - static String table4 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t4").replaceAll("\'", ""); - static String procName = RandomUtil.getIdentifier("UnitStatement_LimitEscape_p1").replaceAll("\'", ""); + static String table1 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t1").replaceAll("", ""); + static String table2 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t2").replaceAll("", ""); + static String table3 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t3").replaceAll("", ""); + static String table4 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t4").replaceAll("", ""); + static String procName = RandomUtil.getIdentifier("UnitStatement_LimitEscape_p1").replaceAll("", ""); static class Query { String inputSql, outputSql; From 83fecee09d71127b9e94664976093de79642abca Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 7 Jun 2024 10:45:33 -0700 Subject: [PATCH 17/57] Undo LimitEscapeTest changes --- .../sqlserver/jdbc/unit/statement/LimitEscapeTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java index b52dd4a69..e54b6760f 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java @@ -46,11 +46,11 @@ public class LimitEscapeTest extends AbstractTest { private static Vector offsetQuery = new Vector<>(); // TODO: remove quote for now to avoid bug in driver - static String table1 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t1").replaceAll("", ""); - static String table2 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t2").replaceAll("", ""); - static String table3 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t3").replaceAll("", ""); - static String table4 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t4").replaceAll("", ""); - static String procName = RandomUtil.getIdentifier("UnitStatement_LimitEscape_p1").replaceAll("", ""); + static String table1 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t1").replaceAll("\'", ""); + static String table2 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t2").replaceAll("\'", ""); + static String table3 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t3").replaceAll("\'", ""); + static String table4 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t4").replaceAll("\'", ""); + static String procName = RandomUtil.getIdentifier("UnitStatement_LimitEscape_p1").replaceAll("\'", ""); static class Query { String inputSql, outputSql; From 1f3e8917638005725ef7b0a4f4c03b0561fa85be Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 7 Jun 2024 10:46:36 -0700 Subject: [PATCH 18/57] Remove redundant files --- .../microsoft/sqlserver/jdbc/ConfigRead.java | 217 ------------------ .../sqlserver/jdbc/ConfigRetryRule.java | 183 --------------- 2 files changed, 400 deletions(-) delete mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java delete mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java deleted file mode 100644 index db6a39e03..000000000 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRead.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.microsoft.sqlserver.jdbc; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.net.URI; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; - - -public class ConfigRead { - private final static int intervalBetweenReads = 30000; // How many ms must have elapsed before we re-read - private final static String defaultPropsFile = "mssql-jdbc.properties"; - private static ConfigRead driverInstance = null; - private static long timeLastModified; - private static long lastTimeRead; - private static String lastQuery = ""; - private static String customRetryRules = ""; // Rules imported from connection string - private static boolean replaceFlag; // Are we replacing the list of transient errors (for connection retry)? - private static HashMap cxnRules = new HashMap<>(); - private static HashMap stmtRules = new HashMap<>(); - - private ConfigRead() throws SQLServerException { - // On instantiation, set last time read and set up rules - lastTimeRead = new Date().getTime(); - setUpRules(); - } - - /** - * Fetches the static instance of ConfigRead, instantiating it if it hasn't already been. - * - * @return The static instance of ConfigRead - * @throws SQLServerException - * an exception - */ - public static synchronized ConfigRead getInstance() throws SQLServerException { - // Every time we fetch this static instance, instantiate if it hasn't been. If it has then re-read and return - // the instance. - if (driverInstance == null) { - driverInstance = new ConfigRead(); - } else { - reread(); - } - - return driverInstance; - } - - /** - * Check if it's time to re-read, and if the file has changed. If so, then re-set up rules. - * - * @throws SQLServerException - * an exception - */ - private static void reread() throws SQLServerException { - long currentTime = new Date().getTime(); - - if ((currentTime - lastTimeRead) >= intervalBetweenReads && !compareModified()) { - lastTimeRead = currentTime; - setUpRules(); - } - } - - private static boolean compareModified() { - String inputToUse = getCurrentClassPath() + defaultPropsFile; - - try { - File f = new File(inputToUse); - return f.lastModified() == timeLastModified; - } catch (Exception e) { - return false; - } - } - - public void setCustomRetryRules(String cRR) throws SQLServerException { - customRetryRules = cRR; - setUpRules(); - } - - public void setFromConnectionString(String custom) throws SQLServerException { - if (!custom.isEmpty()) { - setCustomRetryRules(custom); - } - } - - public void storeLastQuery(String sql) { - lastQuery = sql.toLowerCase(); - } - - public String getLastQuery() { - return lastQuery; - } - - private static void setUpRules() throws SQLServerException { - LinkedList temp = null; - - if (!customRetryRules.isEmpty()) { - // If user as set custom rules in connection string, then we use those over any file - temp = new LinkedList<>(); - for (String s : customRetryRules.split(";")) { - temp.add(s); - } - } else { - try { - temp = readFromFile(); - } catch (IOException e) { - // TODO handle IO exception - } - } - - if (temp != null) { - createRules(temp); - } - } - - private static void createRules(LinkedList list) throws SQLServerException { - cxnRules = new HashMap<>(); - stmtRules = new HashMap<>(); - - for (String temp : list) { - - ConfigRetryRule rule = new ConfigRetryRule(temp); - if (rule.getError().contains(",")) { - - String[] arr = rule.getError().split(","); - - for (String s : arr) { - ConfigRetryRule rulez = new ConfigRetryRule(s, rule); - if (rule.getConnectionStatus()) { - if (rule.getReplaceExisting()) { - cxnRules = new HashMap<>(); - replaceFlag = true; - } - cxnRules.put(Integer.parseInt(rulez.getError()), rulez); - } else { - stmtRules.put(Integer.parseInt(rulez.getError()), rulez); - } - } - } else { - if (rule.getConnectionStatus()) { - if (rule.getReplaceExisting()) { - cxnRules = new HashMap<>(); - replaceFlag = true; - } - cxnRules.put(Integer.parseInt(rule.getError()), rule); - } else { - stmtRules.put(Integer.parseInt(rule.getError()), rule); - } - } - } - } - - private static String getCurrentClassPath() { - try { - String className = new Object() {}.getClass().getEnclosingClass().getName(); - String location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath(); - location = location.substring(0, location.length() - 16); - URI uri = new URI(location + "/"); - return uri.getPath(); - } catch (Exception e) { - // TODO handle exception - } - return null; - } - - private static LinkedList readFromFile() throws IOException { - String filePath = getCurrentClassPath(); - - LinkedList list = new LinkedList<>(); - try { - File f = new File(filePath + defaultPropsFile); - timeLastModified = f.lastModified(); - try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { - String readLine; - - while ((readLine = buffer.readLine()) != null) { - if (readLine.startsWith("retryExec")) { - String value = readLine.split("=")[1]; - for (String s : value.split(";")) { - list.add(s); - } - } - // list.add(readLine); - } - } - } catch (IOException e) { - // TODO handle IO Exception - throw new IOException(); - } - return list; - } - - public ConfigRetryRule searchRuleSet(int ruleToSearch, String ruleSet) throws SQLServerException { - reread(); - if (ruleSet.equals("statement")) { - for (Map.Entry entry : stmtRules.entrySet()) { - if (entry.getKey() == ruleToSearch) { - return entry.getValue(); - } - } - } else { - for (Map.Entry entry : cxnRules.entrySet()) { - if (entry.getKey() == ruleToSearch) { - return entry.getValue(); - } - } - } - - return null; - } - - public boolean getReplaceFlag() { - return replaceFlag; - } -} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java deleted file mode 100644 index cc9adcc9e..000000000 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.microsoft.sqlserver.jdbc; - -import java.text.MessageFormat; -import java.util.ArrayList; - - -public class ConfigRetryRule { - private String retryError; - private String operand = "*"; - private int initialRetryTime = 10; - private int retryChange = 2; - private int retryCount; - private String retryQueries = ""; - private ArrayList waitTimes = new ArrayList<>(); - private boolean isConnection = false; - private boolean replaceExisting = false; - - public ConfigRetryRule(String s) throws SQLServerException { - String[] stArr = parse(s); - addElements(stArr); - calcWaitTime(); - } - - public ConfigRetryRule(String rule, ConfigRetryRule r) { - copyFromCopy(r); - this.retryError = rule; - } - - private void copyFromCopy(ConfigRetryRule r) { - this.retryError = r.getError(); - this.operand = r.getOperand(); - this.initialRetryTime = r.getInitialRetryTime(); - this.retryChange = r.getRetryChange(); - this.retryCount = r.getRetryCount(); - this.retryQueries = r.getRetryQueries(); - this.waitTimes = r.getWaitTimes(); - this.isConnection = r.getConnectionStatus(); - } - - private String[] parse(String s) { - String temp = s + " "; - - temp = temp.replace(": ", ":0"); - temp = temp.replace("{", ""); - temp = temp.replace("}", ""); - temp = temp.trim(); - - // We want to do an empty string check here - - if (temp.isEmpty()) { - - } - - return temp.split(":"); - } - - private void addElements(String[] s) throws SQLServerException { - // +"retryExec={2714,2716:1,2*2:CREATE;2715:1,3;+4060,4070};" - if (s.length == 1) { - // If single element, connection - isConnection = true; - retryError = appendOrReplace(s[0]); - } else if (s.length == 2 || s.length == 3) { - // If 2 or 3, statement, either with or without query - // Parse first element (statement rules) - if (!StringUtils.isNumeric(s[0])) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); - Object[] msgArgs = {s[0], "\"Retry Error\""}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, true); - } else { - retryError = s[0]; - } - - // Parse second element (retry options) - String[] st = s[1].split(","); - - // We have retry count AND timing rules - retryCount = Integer.parseInt(st[0]); // TODO input validation - // Second half can either be N, N OP, N OP N - if (st[1].contains("*")) { - // We know its either N OP, N OP N - String[] sss = st[1].split("/*"); - initialRetryTime = Integer.parseInt(sss[0]); - operand = "*"; - if (sss.length > 2) { - retryChange = Integer.parseInt(sss[2]); - } - } else if (st[1].contains("+")) { - // We know its either N OP, N OP N - String[] sss = st[1].split("/+"); - initialRetryTime = Integer.parseInt(sss[0]); - operand = "*"; - if (sss.length > 2) { - retryChange = Integer.parseInt(sss[2]); - } - } else { - initialRetryTime = Integer.parseInt(st[1]); - // TODO set defaults - } - if (s.length == 3) { - // Query has also been provided - retryQueries = (s[2].equals("0") ? "" : s[2].toLowerCase()); - } - } else { - // If the length is not 1,2,3, then the provided option is invalid - // Prov - - StringBuilder builder = new StringBuilder(); - - for (String string : s) { - builder.append(string); - } - - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); - Object[] msgArgs = {builder.toString()}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, true); - } - } - - private String appendOrReplace(String s) { - if (s.charAt(0) == '+') { - replaceExisting = false; - StringUtils.isNumeric(s.substring(1)); - return s.substring(1); - } else { - replaceExisting = true; - return s; - } - } - - public void calcWaitTime() { - for (int i = 0; i < retryCount; ++i) { - int waitTime = initialRetryTime; - if (operand.equals("+")) { - for (int j = 0; j < i; ++j) { - waitTime += retryChange; - } - } else if (operand.equals("*")) { - for (int k = 0; k < i; ++k) { - waitTime *= retryChange; - } - - } - waitTimes.add(waitTime); - } - } - - public String getError() { - return retryError; - } - - public String getOperand() { - return operand; - } - - public int getInitialRetryTime() { - return initialRetryTime; - } - - public int getRetryChange() { - return retryChange; - } - - public int getRetryCount() { - return retryCount; - } - - public boolean getConnectionStatus() { - return isConnection; - } - - public String getRetryQueries() { - return retryQueries; - } - - public ArrayList getWaitTimes() { - return waitTimes; - } - - public boolean getReplaceExisting() { - return replaceExisting; - } -} From e2b2bd060a37682f4e1418446c8be6944b2a79f3 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 10 Jun 2024 09:51:07 -0700 Subject: [PATCH 19/57] Final? --- mssql-jdbc.properties | 1 + .../jdbc/ConfigurableRetryLogicTest.java | 82 +++++++++++-------- 2 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 mssql-jdbc.properties diff --git a/mssql-jdbc.properties b/mssql-jdbc.properties new file mode 100644 index 000000000..ae52a1074 --- /dev/null +++ b/mssql-jdbc.properties @@ -0,0 +1 @@ +retryExec={2714:1,5} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index b2181695d..053b2d5d6 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -10,6 +10,9 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.concurrent.TimeUnit; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -215,6 +218,26 @@ public void statementTimingTests() { } } + @Test + public void statementTimingTestFromFile() { + long totalTime; + long timerStart = System.currentTimeMillis(); + long expectedTime = 5; + + // A single retry waiting 5 seconds + try { + testStatementRetry(""); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(5), + "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(5)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(5 + expectedTime), "total time: " + totalTime + + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(5 + expectedTime)); + } + } + /** * Tests that CRL works with multiple rules provided at once. */ @@ -380,58 +403,53 @@ public void testRetryChange() throws Exception { } @Test - public void testConnectionRetry() throws Exception { - // Test retry with a single connection rule (replace) + public void connectionTimingTest() { + long totalTime; + long timerStart = System.currentTimeMillis(); + long expectedMaxTime = 1; // No retries, expected time < 1 second - // Replace existing rules with our own + // No retries since CRL rules override try { - //testConnectionRetry("blah","retryExec={4060};"); testConnectionRetry("blah","retryExec={9999};"); - } catch (SQLServerException e) { + } catch (Exception e) { assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), + "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); } - // Test retry with a single connection rule (append) - - // Test retry with multiple connection rules -// try { -// testStatementRetry("retryExec={:1,2*2:CREATE};"); -// } catch (SQLServerException e) { -// assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); -// } - } - - @Test - public void connectionTimingTest() throws Exception { - long totalTime; - long timerStart = System.currentTimeMillis(); - long expectedTime = 5; + timerStart = System.currentTimeMillis(); + long expectedMinTime = 20; // (0s attempt + 10s wait + 0s attempt) * 2 due to current driver bug + expectedMaxTime = 25; - // No retries since CRL rules override + // Now retry, timing should reflect this try { - testConnectionRetry("blah","retryExec={9999};"); + testConnectionRetry("blah","retryExec={4060};"); } catch (Exception e) { - Assertions.fail(TestResource.getResource("R_unexpectedException")); + assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); } finally { totalTime = System.currentTimeMillis() - timerStart; - System.out.println("totalTime: " + totalTime); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedTime), - "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedTime)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), + "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), + "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); } timerStart = System.currentTimeMillis(); - // Now retry, timing should reflect this + // Append should work the same way try { - testConnectionRetry("blah","retryExec={4060};"); + testConnectionRetry("blah","retryExec={+4060,4070};"); } catch (Exception e) { - Assertions.fail(TestResource.getResource("R_unexpectedException")); + assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); } finally { totalTime = System.currentTimeMillis() - timerStart; - System.out.println("totalTime: " + totalTime); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedTime), - "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedTime)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), + "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), + "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); } } From 2cb9650c770f84377fb36aa4fc7c6660a906e739 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 10 Jun 2024 09:51:38 -0700 Subject: [PATCH 20/57] Remove mssql-jdpc.properties file --- mssql-jdbc.properties | 1 - 1 file changed, 1 deletion(-) delete mode 100644 mssql-jdbc.properties diff --git a/mssql-jdbc.properties b/mssql-jdbc.properties deleted file mode 100644 index ae52a1074..000000000 --- a/mssql-jdbc.properties +++ /dev/null @@ -1 +0,0 @@ -retryExec={2714:1,5} \ No newline at end of file From 484428898ca510becd7ecde93cd9bbc4fe342f3f Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 10 Jun 2024 10:29:14 -0700 Subject: [PATCH 21/57] sync --> lock --- .../jdbc/ConfigurableRetryLogic.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 872e56e9c..9821b7a8d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -12,6 +12,8 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; public class ConfigurableRetryLogic { @@ -27,6 +29,7 @@ public class ConfigurableRetryLogic { private static boolean replaceFlag = false; // Are we replacing the list of transient errors (for connection retry)? private static HashMap cxnRules = new HashMap<>(); private static HashMap stmtRules = new HashMap<>(); + private static final Lock CRL_LOCK = new ReentrantLock(); private ConfigurableRetryLogic() throws SQLServerException { timeLastRead = new Date().getTime(); @@ -41,13 +44,18 @@ private ConfigurableRetryLogic() throws SQLServerException { * @throws SQLServerException * an exception */ - public static synchronized ConfigurableRetryLogic getInstance() throws SQLServerException { - if (driverInstance == null) { - driverInstance = new ConfigurableRetryLogic(); - } else { - reread(); + public static ConfigurableRetryLogic getInstance() throws SQLServerException { + CRL_LOCK.lock(); + try { + if (driverInstance == null) { + driverInstance = new ConfigurableRetryLogic(); + } else { + reread(); + } + return driverInstance; + } finally { + CRL_LOCK.unlock(); } - return driverInstance; } private static void reread() throws SQLServerException { From bb3b2530255a25b6f8ad5ad89d9efc26efc64a33 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 10 Jun 2024 11:22:58 -0700 Subject: [PATCH 22/57] Remove problematic test --- .../jdbc/ConfigurableRetryLogicTest.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 053b2d5d6..69350f95d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -218,26 +218,6 @@ public void statementTimingTests() { } } - @Test - public void statementTimingTestFromFile() { - long totalTime; - long timerStart = System.currentTimeMillis(); - long expectedTime = 5; - - // A single retry waiting 5 seconds - try { - testStatementRetry(""); - } catch (Exception e) { - Assertions.fail(TestResource.getResource("R_unexpectedException")); - } finally { - totalTime = System.currentTimeMillis() - timerStart; - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(5), - "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(5)); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(5 + expectedTime), "total time: " + totalTime - + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(5 + expectedTime)); - } - } - /** * Tests that CRL works with multiple rules provided at once. */ From 00c3545ebd61fdab5bb56b23ea2e7b7177e57105 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 10 Jun 2024 13:16:46 -0700 Subject: [PATCH 23/57] Since error is unclear, try removing last test --- .../jdbc/ConfigurableRetryLogicTest.java | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 69350f95d..864f3a546 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -382,56 +382,56 @@ public void testRetryChange() throws Exception { } } - @Test - public void connectionTimingTest() { - long totalTime; - long timerStart = System.currentTimeMillis(); - long expectedMaxTime = 1; // No retries, expected time < 1 second - - - // No retries since CRL rules override - try { - testConnectionRetry("blah","retryExec={9999};"); - } catch (Exception e) { - assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); - } finally { - totalTime = System.currentTimeMillis() - timerStart; - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), - "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); - } - - timerStart = System.currentTimeMillis(); - long expectedMinTime = 20; // (0s attempt + 10s wait + 0s attempt) * 2 due to current driver bug - expectedMaxTime = 25; - - // Now retry, timing should reflect this - try { - testConnectionRetry("blah","retryExec={4060};"); - } catch (Exception e) { - assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); - } finally { - totalTime = System.currentTimeMillis() - timerStart; - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), - "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), - "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); - } - - timerStart = System.currentTimeMillis(); - - // Append should work the same way - try { - testConnectionRetry("blah","retryExec={+4060,4070};"); - } catch (Exception e) { - assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); - } finally { - totalTime = System.currentTimeMillis() - timerStart; - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), - "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), - "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); - } - } +// @Test +// public void connectionTimingTest() { +// long totalTime; +// long timerStart = System.currentTimeMillis(); +// long expectedMaxTime = 1; // No retries, expected time < 1 second +// +// +// // No retries since CRL rules override +// try { +// testConnectionRetry("blah","retryExec={9999};"); +// } catch (Exception e) { +// assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); +// } finally { +// totalTime = System.currentTimeMillis() - timerStart; +// assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), +// "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); +// } +// +// timerStart = System.currentTimeMillis(); +// long expectedMinTime = 20; // (0s attempt + 10s wait + 0s attempt) * 2 due to current driver bug +// expectedMaxTime = 25; +// +// // Now retry, timing should reflect this +// try { +// testConnectionRetry("blah","retryExec={4060};"); +// } catch (Exception e) { +// assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); +// } finally { +// totalTime = System.currentTimeMillis() - timerStart; +// assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), +// "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); +// assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), +// "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); +// } +// +// timerStart = System.currentTimeMillis(); +// +// // Append should work the same way +// try { +// testConnectionRetry("blah","retryExec={+4060,4070};"); +// } catch (Exception e) { +// assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); +// } finally { +// totalTime = System.currentTimeMillis() - timerStart; +// assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), +// "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); +// assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), +// "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); +// } +// } private static void createTable(Statement stmt) throws SQLException { String sql = "create table " + tableName + " (c1 int null);"; From 1e79a1faad9ade42a3e90187fa95183d580ec5d0 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 10 Jun 2024 13:49:11 -0700 Subject: [PATCH 24/57] Adding back connection test --- .../jdbc/ConfigurableRetryLogicTest.java | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 864f3a546..e2406722a 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -382,56 +382,59 @@ public void testRetryChange() throws Exception { } } -// @Test -// public void connectionTimingTest() { -// long totalTime; -// long timerStart = System.currentTimeMillis(); -// long expectedMaxTime = 1; // No retries, expected time < 1 second -// -// -// // No retries since CRL rules override -// try { -// testConnectionRetry("blah","retryExec={9999};"); -// } catch (Exception e) { -// assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); -// } finally { -// totalTime = System.currentTimeMillis() - timerStart; -// assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), -// "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); -// } -// -// timerStart = System.currentTimeMillis(); -// long expectedMinTime = 20; // (0s attempt + 10s wait + 0s attempt) * 2 due to current driver bug -// expectedMaxTime = 25; -// -// // Now retry, timing should reflect this -// try { -// testConnectionRetry("blah","retryExec={4060};"); -// } catch (Exception e) { -// assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); -// } finally { -// totalTime = System.currentTimeMillis() - timerStart; -// assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), -// "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); -// assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), -// "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); -// } -// -// timerStart = System.currentTimeMillis(); -// -// // Append should work the same way -// try { -// testConnectionRetry("blah","retryExec={+4060,4070};"); -// } catch (Exception e) { -// assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); -// } finally { -// totalTime = System.currentTimeMillis() - timerStart; -// assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), -// "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); -// assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), -// "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); -// } -// } + @Test + public void connectionTimingTest() { + long totalTime; + long timerStart = System.currentTimeMillis(); + long expectedMaxTime = 1; // No retries, expected time < 1 second + + + // No retries since CRL rules override + try { + testConnectionRetry("blah","retryExec={9999};"); + } catch (Exception e) { + assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + System.out.println(totalTime); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), + "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + } + + timerStart = System.currentTimeMillis(); + long expectedMinTime = 20; // (0s attempt + 10s wait + 0s attempt) * 2 due to current driver bug + expectedMaxTime = 25; + + // Now retry, timing should reflect this + try { + testConnectionRetry("blah","retryExec={4060};"); + } catch (Exception e) { + assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + System.out.println(totalTime); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), + "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), + "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); + } + + timerStart = System.currentTimeMillis(); + + // Append should work the same way + try { + testConnectionRetry("blah","retryExec={+4060,4070};"); + } catch (Exception e) { + assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); + } finally { + totalTime = System.currentTimeMillis() - timerStart; + System.out.println(totalTime); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), + "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), + "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); + } + } private static void createTable(Statement stmt) throws SQLException { String sql = "create table " + tableName + " (c1 int null);"; From f8273ea6a38276ead7ab4dc66bcfa5dcb16e0cac Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 10 Jun 2024 14:19:53 -0700 Subject: [PATCH 25/57] I need debugging --- .../microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index e2406722a..9afe1a670 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -393,6 +393,7 @@ public void connectionTimingTest() { try { testConnectionRetry("blah","retryExec={9999};"); } catch (Exception e) { + System.out.println("9999------------------Error: " + e.getMessage() + "------------------"); assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); } finally { totalTime = System.currentTimeMillis() - timerStart; @@ -409,6 +410,7 @@ public void connectionTimingTest() { try { testConnectionRetry("blah","retryExec={4060};"); } catch (Exception e) { + System.out.println("4060------------------Error: " + e.getMessage() + "------------------"); assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); } finally { totalTime = System.currentTimeMillis() - timerStart; @@ -425,6 +427,7 @@ public void connectionTimingTest() { try { testConnectionRetry("blah","retryExec={+4060,4070};"); } catch (Exception e) { + System.out.println("+4060------------------Error: " + e.getMessage() + "------------------"); assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); } finally { totalTime = System.currentTimeMillis() - timerStart; From a74cffeef56849840b8919196b7cc5c353867d61 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 10 Jun 2024 16:28:31 -0700 Subject: [PATCH 26/57] Fix for MI --- .../jdbc/ConfigurableRetryLogicTest.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 9afe1a670..bc2563f59 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -394,7 +394,14 @@ public void connectionTimingTest() { testConnectionRetry("blah","retryExec={9999};"); } catch (Exception e) { System.out.println("9999------------------Error: " + e.getMessage() + "------------------"); - assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) + || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() + .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), + e.getMessage()); } finally { totalTime = System.currentTimeMillis() - timerStart; System.out.println(totalTime); @@ -411,7 +418,14 @@ public void connectionTimingTest() { testConnectionRetry("blah","retryExec={4060};"); } catch (Exception e) { System.out.println("4060------------------Error: " + e.getMessage() + "------------------"); - assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) + || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() + .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), + e.getMessage()); } finally { totalTime = System.currentTimeMillis() - timerStart; System.out.println(totalTime); @@ -428,7 +442,14 @@ public void connectionTimingTest() { testConnectionRetry("blah","retryExec={+4060,4070};"); } catch (Exception e) { System.out.println("+4060------------------Error: " + e.getMessage() + "------------------"); - assertTrue(e.getMessage().startsWith(TestResource.getResource("R_cannotOpenDatabase"))); + assertTrue( + (e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) + || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() + .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) + || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), + e.getMessage()); } finally { totalTime = System.currentTimeMillis() - timerStart; System.out.println(totalTime); From 5d80ecd41051f1cd278d6ef1bd52f3b9b77e4752 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 11 Jun 2024 09:58:50 -0700 Subject: [PATCH 27/57] if condition for min time assertion --- .../jdbc/ConfigurableRetryLogicTest.java | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index bc2563f59..29acc27c8 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -387,6 +387,7 @@ public void connectionTimingTest() { long totalTime; long timerStart = System.currentTimeMillis(); long expectedMaxTime = 1; // No retries, expected time < 1 second + Exception ex = null; // No retries since CRL rules override @@ -394,19 +395,24 @@ public void connectionTimingTest() { testConnectionRetry("blah","retryExec={9999};"); } catch (Exception e) { System.out.println("9999------------------Error: " + e.getMessage() + "------------------"); + ex = e; assertTrue( (e.getMessage().toLowerCase() .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) - || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), + || ((isSqlAzure() || isSqlAzureDW()) && e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), e.getMessage()); } finally { totalTime = System.currentTimeMillis() - timerStart; System.out.println(totalTime); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), - "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + if (null != ex && ex.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) { + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), + "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + } + } timerStart = System.currentTimeMillis(); @@ -418,21 +424,25 @@ public void connectionTimingTest() { testConnectionRetry("blah","retryExec={4060};"); } catch (Exception e) { System.out.println("4060------------------Error: " + e.getMessage() + "------------------"); + ex = e; assertTrue( (e.getMessage().toLowerCase() .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) - || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), + || ((isSqlAzure() || isSqlAzureDW()) && e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), e.getMessage()); } finally { totalTime = System.currentTimeMillis() - timerStart; System.out.println(totalTime); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), - "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), - "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); + if (null != ex && ex.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) { + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), + "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), + "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); + } } timerStart = System.currentTimeMillis(); @@ -442,21 +452,25 @@ public void connectionTimingTest() { testConnectionRetry("blah","retryExec={+4060,4070};"); } catch (Exception e) { System.out.println("+4060------------------Error: " + e.getMessage() + "------------------"); + ex = e; assertTrue( (e.getMessage().toLowerCase() .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) - || ((isSqlAzure() || isSqlAzureDW()) ? e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_connectTimedOut").toLowerCase()) : false), + || ((isSqlAzure() || isSqlAzureDW()) && e.getMessage().toLowerCase() + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), e.getMessage()); } finally { totalTime = System.currentTimeMillis() - timerStart; System.out.println(totalTime); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), - "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), - "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); + if (null != ex && ex.getMessage().toLowerCase() + .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) { + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), + "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), + "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); + } } } From 11e84cc0656ebecb66ffda9bccba27f320671978 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Thu, 13 Jun 2024 11:03:02 -0700 Subject: [PATCH 28/57] Leftover debug code, cleanup --- .../jdbc/ConfigurableRetryLogic.java | 8 +- .../sqlserver/jdbc/SQLServerConnection.java | 2 +- .../sqlserver/jdbc/SQLServerResource.java | 2 +- .../jdbc/ConfigurableRetryLogicTest.java | 100 +++++++++--------- .../jdbc/unit/statement/LimitEscapeTest.java | 2 +- 5 files changed, 56 insertions(+), 58 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 9821b7a8d..c42ecd28f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -17,7 +17,7 @@ public class ConfigurableRetryLogic { - private final static int INTERVAL_BETWEEN_READS = 30000; // How many ms must have elapsed before we re-read + private final static int INTERVAL_BETWEEN_READS = 30000; private final static String DEFAULT_PROPS_FILE = "mssql-jdbc.properties"; private static final java.util.logging.Logger CONFIGURABLE_RETRY_LOGGER = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); @@ -26,7 +26,7 @@ public class ConfigurableRetryLogic { private static long timeLastRead; private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) private static String rulesFromConnectionString = ""; - private static boolean replaceFlag = false; // Are we replacing the list of transient errors (for connection retry)? + private static boolean replaceFlag = false; // Are we replacing the list of transient errors? private static HashMap cxnRules = new HashMap<>(); private static HashMap stmtRules = new HashMap<>(); private static final Lock CRL_LOCK = new ReentrantLock(); @@ -37,8 +37,8 @@ private ConfigurableRetryLogic() throws SQLServerException { } /** - * Fetches the static instance of ConfigurableRetryLogic, instantiating it if it hasn't already been. Each time the instance - * is fetched, we check if a re-read is needed, and do so if properties should be re-read. + * Fetches the static instance of ConfigurableRetryLogic, instantiating it if it hasn't already been. + * Each time the instance is fetched, we check if a re-read is needed, and do so if properties should be re-read. * * @return The static instance of ConfigurableRetryLogic * @throws SQLServerException diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index a578aeb11..c5ba895cd 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -2015,6 +2015,7 @@ Connection connect(Properties propsIn, SQLServerPooledConnection pooledConnectio } throw e; } else { + // Only retry if matches configured CRL rules, or transient error (if CRL is not in use) SQLServerError sqlServerError = e.getSQLServerError(); if (null == sqlServerError) { @@ -2371,7 +2372,6 @@ Connection connectInternal(Properties propsIn, activeConnectionProperties.setProperty(sPropKey, sPropValue); } retryExec = sPropValue; - // ConfigurableRetryLogic.getInstance().setCustomRetryRules(sPropValue); ConfigurableRetryLogic.getInstance().setFromConnectionString(sPropValue); sPropKey = SQLServerDriverBooleanProperty.CALC_BIG_DECIMAL_PRECISION.toString(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 68e1ae9db..85ec1cfeb 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -519,7 +519,7 @@ protected Object[][] getContents() { {"R_InvalidCSVQuotes", "Failed to parse the CSV file, verify that the fields are correctly enclosed in double quotes."}, {"R_TokenRequireUrl", "Token credentials require a URL using the HTTPS protocol scheme."}, {"R_calcBigDecimalPrecisionPropertyDescription", "Indicates whether the driver should calculate precision for big decimal values."}, - {"R_retryExecPropertyDescription", "Indicates whether the driver should calculate precision for big decimal values."}, + {"R_retryExecPropertyDescription", "List of rules to follow for configurable retry logic."}, {"R_maxResultBufferPropertyDescription", "Determines maximum amount of bytes that can be read during retrieval of result set"}, {"R_maxResultBufferInvalidSyntax", "Invalid syntax: {0} in maxResultBuffer parameter."}, {"R_maxResultBufferNegativeParameterValue", "MaxResultBuffer must have positive value: {0}."}, diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 29acc27c8..6967dd5c9 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -10,9 +10,6 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.concurrent.TimeUnit; -import java.util.logging.ConsoleHandler; -import java.util.logging.Level; -import java.util.logging.Logger; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -95,7 +92,7 @@ public void testStatementRetryCallableStatement() throws Exception { * if unable to connect or execute against db */ public void testStatementRetry(String addedRetryParams) throws Exception { - String cxnString = connectionString + addedRetryParams; // 2714 There is already an object named x + String cxnString = connectionString + addedRetryParams; try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { try { @@ -111,14 +108,18 @@ public void testStatementRetry(String addedRetryParams) throws Exception { } } + /** + * Tests connection retry. Used in other tests. + * + * @throws Exception + * if unable to connect or execute against db + */ public void testConnectionRetry(String replacedDbName, String addedRetryParams) throws Exception { - String cxnString = connectionString + addedRetryParams; // 2714 There is already an object named x + String cxnString = connectionString + addedRetryParams; cxnString = TestUtils.addOrOverrideProperty(cxnString, "database", replacedDbName); try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { try { - //createTable(s); - //s.execute("create table " + tableName + " (c1 int null);"); fail(TestResource.getResource("R_expectedFailPassed")); } catch (Exception e) { System.out.println("blah"); @@ -137,7 +138,7 @@ public void testConnectionRetry(String replacedDbName, String addedRetryParams) * if unable to connect or execute against db */ public void testStatementRetryWithShortQueryTimeout(String addedRetryParams) throws Exception { - String cxnString = connectionString + addedRetryParams; // 2714 There is already an object named x + String cxnString = connectionString + addedRetryParams; try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { try { @@ -256,7 +257,6 @@ public void testCorrectlyFormattedRules() { // Test length 1 testStatementRetry("retryExec={4060};"); testStatementRetry("retryExec={+4060,4070};"); - testStatementRetry("retryExec={2714:1;};"); // Test length 2 @@ -264,9 +264,11 @@ public void testCorrectlyFormattedRules() { // Test length 3, also multiple statement errors testStatementRetry("retryExec={2714,2716:1,2*2:CREATE};"); + // Same as above but using + operator testStatementRetry("retryExec={2714,2716:1,2+2:CREATE};"); testStatementRetry("retryExec={2714,2716:1,2+2};"); + } catch (Exception e) { Assertions.fail(TestResource.getResource("R_unexpectedException")); } @@ -382,66 +384,64 @@ public void testRetryChange() throws Exception { } } + /** + * Tests that the correct number of retries are happening for all connection scenarios. Tests are expected to take + * a minimum of the sum of whatever has been defined for the waiting intervals, and maximum of the previous sum + * plus some amount of time to account for test environment slowness. + */ @Test public void connectionTimingTest() { long totalTime; long timerStart = System.currentTimeMillis(); - long expectedMaxTime = 1; // No retries, expected time < 1 second - Exception ex = null; - + long expectedMaxTime = 1; - // No retries since CRL rules override + // No retries since CRL rules override, expected time ~1 second try { - testConnectionRetry("blah","retryExec={9999};"); + testConnectionRetry("blah", "retryExec={9999};"); } catch (Exception e) { - System.out.println("9999------------------Error: " + e.getMessage() + "------------------"); - ex = e; assertTrue( (e.getMessage().toLowerCase() .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() - .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) + .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) || ((isSqlAzure() || isSqlAzureDW()) && e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), e.getMessage()); - } finally { - totalTime = System.currentTimeMillis() - timerStart; - System.out.println(totalTime); - if (null != ex && ex.getMessage().toLowerCase() + + if (e.getMessage().toLowerCase() .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) { + // Only check the timing if the correct error, "cannot open database", is returned. + totalTime = System.currentTimeMillis() - timerStart; assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); } - } timerStart = System.currentTimeMillis(); - long expectedMinTime = 20; // (0s attempt + 10s wait + 0s attempt) * 2 due to current driver bug + long expectedMinTime = 20; expectedMaxTime = 25; - // Now retry, timing should reflect this + // (0s attempt + 10s wait + 0s attempt) * 2 due to current driver bug = expected 20s execution time try { - testConnectionRetry("blah","retryExec={4060};"); + testConnectionRetry("blah", "retryExec={4060};"); } catch (Exception e) { - System.out.println("4060------------------Error: " + e.getMessage() + "------------------"); - ex = e; assertTrue( (e.getMessage().toLowerCase() .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() - .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) + .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) || ((isSqlAzure() || isSqlAzureDW()) && e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), e.getMessage()); - } finally { - totalTime = System.currentTimeMillis() - timerStart; - System.out.println(totalTime); - if (null != ex && ex.getMessage().toLowerCase() + + if (e.getMessage().toLowerCase() .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) { - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), - "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), - "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); + // Only check the timing if the correct error, "cannot open database", is returned. + totalTime = System.currentTimeMillis() - timerStart; + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), "total time: " + totalTime + + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), "total time: " + totalTime + + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); } } @@ -449,27 +449,25 @@ public void connectionTimingTest() { // Append should work the same way try { - testConnectionRetry("blah","retryExec={+4060,4070};"); + testConnectionRetry("blah", "retryExec={+4060,4070};"); } catch (Exception e) { - System.out.println("+4060------------------Error: " + e.getMessage() + "------------------"); - ex = e; assertTrue( (e.getMessage().toLowerCase() .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() - .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) + .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) || ((isSqlAzure() || isSqlAzureDW()) && e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), + .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), e.getMessage()); - } finally { - totalTime = System.currentTimeMillis() - timerStart; - System.out.println(totalTime); - if (null != ex && ex.getMessage().toLowerCase() + + if (e.getMessage().toLowerCase() .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) { - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), - "total time: " + totalTime + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), - "total time: " + totalTime + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); + // Only check the timing if the correct error, "cannot open database", is returned. + totalTime = System.currentTimeMillis() - timerStart; + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), "total time: " + totalTime + + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), "total time: " + totalTime + + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); } } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java index e54b6760f..3101d0891 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java @@ -46,7 +46,7 @@ public class LimitEscapeTest extends AbstractTest { private static Vector offsetQuery = new Vector<>(); // TODO: remove quote for now to avoid bug in driver - static String table1 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t1").replaceAll("\'", ""); + static String table1 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t1").replaceAll("", ""); static String table2 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t2").replaceAll("\'", ""); static String table3 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t3").replaceAll("\'", ""); static String table4 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t4").replaceAll("\'", ""); From 141fc0c344a55c8f5591e4aa5ff92e77e9d2b3e9 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Thu, 13 Jun 2024 11:35:23 -0700 Subject: [PATCH 29/57] Mistaken changes committed --- .../sqlserver/jdbc/unit/statement/LimitEscapeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java index 3101d0891..e54b6760f 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/LimitEscapeTest.java @@ -46,7 +46,7 @@ public class LimitEscapeTest extends AbstractTest { private static Vector offsetQuery = new Vector<>(); // TODO: remove quote for now to avoid bug in driver - static String table1 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t1").replaceAll("", ""); + static String table1 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t1").replaceAll("\'", ""); static String table2 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t2").replaceAll("\'", ""); static String table3 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t3").replaceAll("\'", ""); static String table4 = RandomUtil.getIdentifier("UnitStatement_LimitEscape_t4").replaceAll("\'", ""); From 9f24ce4aa55978cc22e815e47cbaaac2ac482590 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Thu, 13 Jun 2024 14:40:44 -0700 Subject: [PATCH 30/57] More liberal time windows --- .../jdbc/ConfigurableRetryLogicTest.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 6967dd5c9..473ad612b 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -160,7 +160,6 @@ public void testStatementRetryWithShortQueryTimeout(String addedRetryParams) thr public void statementTimingTests() { long totalTime; long timerStart = System.currentTimeMillis(); - long expectedTime = 5; // A single retry immediately try { @@ -169,8 +168,8 @@ public void statementTimingTests() { Assertions.fail(TestResource.getResource("R_unexpectedException")); } finally { totalTime = System.currentTimeMillis() - timerStart; - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedTime), - "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedTime)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(10), + "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(10)); } timerStart = System.currentTimeMillis(); @@ -184,8 +183,8 @@ public void statementTimingTests() { totalTime = System.currentTimeMillis() - timerStart; assertTrue(totalTime > TimeUnit.SECONDS.toMillis(5), "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(5)); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(5 + expectedTime), "total time: " + totalTime - + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(5 + expectedTime)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(15), "total time: " + totalTime + + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(15)); } timerStart = System.currentTimeMillis(); @@ -199,8 +198,8 @@ public void statementTimingTests() { totalTime = System.currentTimeMillis() - timerStart; assertTrue(totalTime > TimeUnit.SECONDS.toMillis(8), "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(8)); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(8 + expectedTime), "total time: " + totalTime - + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(8 + expectedTime)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(18), "total time: " + totalTime + + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(18)); } timerStart = System.currentTimeMillis(); @@ -214,8 +213,8 @@ public void statementTimingTests() { totalTime = System.currentTimeMillis() - timerStart; assertTrue(totalTime > TimeUnit.SECONDS.toMillis(10), "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(10)); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(10 + expectedTime), "total time: " + totalTime - + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(10 + expectedTime)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(20), "total time: " + totalTime + + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(20)); } } @@ -393,7 +392,7 @@ public void testRetryChange() throws Exception { public void connectionTimingTest() { long totalTime; long timerStart = System.currentTimeMillis(); - long expectedMaxTime = 1; + long expectedMaxTime = 10; // No retries since CRL rules override, expected time ~1 second try { @@ -419,7 +418,7 @@ public void connectionTimingTest() { timerStart = System.currentTimeMillis(); long expectedMinTime = 20; - expectedMaxTime = 25; + expectedMaxTime = 30; // (0s attempt + 10s wait + 0s attempt) * 2 due to current driver bug = expected 20s execution time try { From 04aff85838bf3d1acb3650d1cf6408efc69eab3d Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 18 Jun 2024 09:20:55 -0700 Subject: [PATCH 31/57] Remove connection part --- .../jdbc/ConfigurableRetryLogic.java | 83 +++--------- .../sqlserver/jdbc/SQLServerConnection.java | 19 +-- .../sqlserver/jdbc/SQLServerStatement.java | 3 +- .../jdbc/ConfigurableRetryLogicTest.java | 118 +----------------- 4 files changed, 25 insertions(+), 198 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index c42ecd28f..4b00f5135 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -1,3 +1,8 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + package com.microsoft.sqlserver.jdbc; import java.io.BufferedReader; @@ -26,8 +31,6 @@ public class ConfigurableRetryLogic { private static long timeLastRead; private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) private static String rulesFromConnectionString = ""; - private static boolean replaceFlag = false; // Are we replacing the list of transient errors? - private static HashMap cxnRules = new HashMap<>(); private static HashMap stmtRules = new HashMap<>(); private static final Lock CRL_LOCK = new ReentrantLock(); @@ -91,9 +94,7 @@ String getLastQuery() { } private static void setUpRules() throws SQLServerException { - cxnRules = new HashMap<>(); stmtRules = new HashMap<>(); - replaceFlag = false; lastQuery = ""; LinkedList temp; @@ -108,7 +109,6 @@ private static void setUpRules() throws SQLServerException { } private static void createRules(LinkedList listOfRules) throws SQLServerException { - cxnRules = new HashMap<>(); stmtRules = new HashMap<>(); for (String potentialRule : listOfRules) { @@ -119,28 +119,10 @@ private static void createRules(LinkedList listOfRules) throws SQLServer for (String retryError : arr) { ConfigRetryRule splitRule = new ConfigRetryRule(retryError, rule); - if (rule.getConnectionStatus()) { - if (rule.getReplaceExisting()) { - if (!replaceFlag) { - cxnRules = new HashMap<>(); - } - replaceFlag = true; - } - cxnRules.put(Integer.parseInt(splitRule.getError()), splitRule); - } else { - stmtRules.put(Integer.parseInt(splitRule.getError()), splitRule); - } + stmtRules.put(Integer.parseInt(splitRule.getError()), splitRule); } } else { - if (rule.getConnectionStatus()) { - if (rule.getReplaceExisting()) { - cxnRules = new HashMap<>(); - replaceFlag = true; - } - cxnRules.put(Integer.parseInt(rule.getError()), rule); - } else { - stmtRules.put(Integer.parseInt(rule.getError()), rule); - } + stmtRules.put(Integer.parseInt(rule.getError()), rule); } } } @@ -184,27 +166,15 @@ private static LinkedList readFromFile() { return list; } - ConfigRetryRule searchRuleSet(int ruleToSearch, String ruleSet) throws SQLServerException { + ConfigRetryRule searchRuleSet(int ruleToSearch) throws SQLServerException { reread(); - if (ruleSet.equals("statement")) { - for (Map.Entry entry : stmtRules.entrySet()) { - if (entry.getKey() == ruleToSearch) { - return entry.getValue(); - } - } - } else { - for (Map.Entry entry : cxnRules.entrySet()) { - if (entry.getKey() == ruleToSearch) { - return entry.getValue(); - } + for (Map.Entry entry : stmtRules.entrySet()) { + if (entry.getKey() == ruleToSearch) { + return entry.getValue(); } } return null; } - - boolean getReplaceFlag() { - return replaceFlag; - } } @@ -216,7 +186,6 @@ class ConfigRetryRule { private int retryCount = 1; private String retryQueries = ""; private ArrayList waitTimes = new ArrayList<>(); - private boolean isConnection = false; private boolean replaceExisting = false; public ConfigRetryRule(String rule) throws SQLServerException { @@ -237,7 +206,6 @@ private void copyFromExisting(ConfigRetryRule base) { this.retryCount = base.getRetryCount(); this.retryQueries = base.getRetryQueries(); this.waitTimes = base.getWaitTimes(); - this.isConnection = base.getConnectionStatus(); } private String[] parse(String rule) { @@ -264,12 +232,7 @@ private void parameterIsNumeric(String value) throws SQLServerException { } private void addElements(String[] rule) throws SQLServerException { - if (rule.length == 1) { - String errorWithoutOptionalPrefix = appendOrReplace(rule[0]); - parameterIsNumeric(errorWithoutOptionalPrefix); - isConnection = true; - retryError = errorWithoutOptionalPrefix; - } else if (rule.length == 2 || rule.length == 3) { + if (rule.length == 2 || rule.length == 3) { parameterIsNumeric(rule[0]); retryError = rule[0]; String[] timings = rule[1].split(","); @@ -286,6 +249,7 @@ private void addElements(String[] rule) throws SQLServerException { if (timings[1].contains("*")) { String[] initialAndChange = timings[1].split("\\*"); parameterIsNumeric(initialAndChange[0]); + initialRetryTime = Integer.parseInt(initialAndChange[0]); operand = "*"; if (initialAndChange.length > 1) { @@ -303,6 +267,8 @@ private void addElements(String[] rule) throws SQLServerException { if (initialAndChange.length > 1) { parameterIsNumeric(initialAndChange[1]); retryChange = Integer.parseInt(initialAndChange[1]); + } else { + retryChange = initialRetryTime; } } else { parameterIsNumeric(timings[1]); @@ -332,17 +298,6 @@ private void addElements(String[] rule) throws SQLServerException { } } - private String appendOrReplace(String retryError) { - if (retryError.charAt(0) == '+') { - replaceExisting = false; - StringUtils.isNumeric(retryError.substring(1)); - return retryError.substring(1); - } else { - replaceExisting = true; - return retryError; - } - } - private void calcWaitTime() { for (int i = 0; i < retryCount; ++i) { int waitTime = initialRetryTime; @@ -379,10 +334,6 @@ public int getRetryCount() { return retryCount; } - public boolean getConnectionStatus() { - return isConnection; - } - public String getRetryQueries() { return retryQueries; } @@ -390,8 +341,4 @@ public String getRetryQueries() { public ArrayList getWaitTimes() { return waitTimes; } - - public boolean getReplaceExisting() { - return replaceExisting; - } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index c5ba895cd..34d693087 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -2015,24 +2015,11 @@ Connection connect(Properties propsIn, SQLServerPooledConnection pooledConnectio } throw e; } else { - // Only retry if matches configured CRL rules, or transient error (if CRL is not in use) - SQLServerError sqlServerError = e.getSQLServerError(); + // only retry if transient error - if (null == sqlServerError) { + SQLServerError sqlServerError = e.getSQLServerError(); + if (!TransientError.isTransientError(sqlServerError)) { throw e; - } else { - ConfigRetryRule rule = ConfigurableRetryLogic.getInstance() - .searchRuleSet(sqlServerError.getErrorNumber(), "connection"); - - if (null == rule) { - if (ConfigurableRetryLogic.getInstance().getReplaceFlag()) { - throw e; - } else { - if (!TransientError.isTransientError(sqlServerError)) { - throw e; - } - } - } } // check if there's time to retry, no point to wait if no time left diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 6f309b5c1..dc3ca41b9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -272,8 +272,7 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ ConfigRetryRule rule = null; if (null != sqlServerError) { - rule = ConfigurableRetryLogic.getInstance().searchRuleSet(e.getSQLServerError().getErrorNumber(), - "statement"); + rule = ConfigurableRetryLogic.getInstance().searchRuleSet(e.getSQLServerError().getErrorNumber()); } if (null != rule && retryAttempt < rule.getRetryCount()) { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 473ad612b..497d23a9a 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -1,3 +1,8 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + package com.microsoft.sqlserver.jdbc; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -20,7 +25,7 @@ /** - * Test connection and statement retry for configurable retry logic + * Test statement retry for configurable retry logic */ public class ConfigurableRetryLogicTest extends AbstractTest { private static String connectionStringCRL = null; @@ -108,29 +113,6 @@ public void testStatementRetry(String addedRetryParams) throws Exception { } } - /** - * Tests connection retry. Used in other tests. - * - * @throws Exception - * if unable to connect or execute against db - */ - public void testConnectionRetry(String replacedDbName, String addedRetryParams) throws Exception { - String cxnString = connectionString + addedRetryParams; - cxnString = TestUtils.addOrOverrideProperty(cxnString, "database", replacedDbName); - - try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { - try { - fail(TestResource.getResource("R_expectedFailPassed")); - } catch (Exception e) { - System.out.println("blah"); - assertTrue(e.getMessage().startsWith("There is already an object"), - TestResource.getResource("R_unexpectedExceptionContent") + ": " + e.getMessage()); - } finally { - dropTable(s); - } - } - } - /** * Tests that statement retry works with statements. A different error is expected here than the test above. * @@ -383,94 +365,6 @@ public void testRetryChange() throws Exception { } } - /** - * Tests that the correct number of retries are happening for all connection scenarios. Tests are expected to take - * a minimum of the sum of whatever has been defined for the waiting intervals, and maximum of the previous sum - * plus some amount of time to account for test environment slowness. - */ - @Test - public void connectionTimingTest() { - long totalTime; - long timerStart = System.currentTimeMillis(); - long expectedMaxTime = 10; - - // No retries since CRL rules override, expected time ~1 second - try { - testConnectionRetry("blah", "retryExec={9999};"); - } catch (Exception e) { - assertTrue( - (e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) - || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() - .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) - || ((isSqlAzure() || isSqlAzureDW()) && e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), - e.getMessage()); - - if (e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) { - // Only check the timing if the correct error, "cannot open database", is returned. - totalTime = System.currentTimeMillis() - timerStart; - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), - "total time: " + totalTime + ", expected time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); - } - } - - timerStart = System.currentTimeMillis(); - long expectedMinTime = 20; - expectedMaxTime = 30; - - // (0s attempt + 10s wait + 0s attempt) * 2 due to current driver bug = expected 20s execution time - try { - testConnectionRetry("blah", "retryExec={4060};"); - } catch (Exception e) { - assertTrue( - (e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) - || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() - .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) - || ((isSqlAzure() || isSqlAzureDW()) && e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), - e.getMessage()); - - if (e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) { - // Only check the timing if the correct error, "cannot open database", is returned. - totalTime = System.currentTimeMillis() - timerStart; - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), "total time: " + totalTime - + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), "total time: " + totalTime - + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); - } - } - - timerStart = System.currentTimeMillis(); - - // Append should work the same way - try { - testConnectionRetry("blah", "retryExec={+4060,4070};"); - } catch (Exception e) { - assertTrue( - (e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) - || (TestUtils.getProperty(connectionString, "msiClientId") != null && e.getMessage() - .toLowerCase().contains(TestResource.getResource("R_loginFailedMI").toLowerCase())) - || ((isSqlAzure() || isSqlAzureDW()) && e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_connectTimedOut").toLowerCase())), - e.getMessage()); - - if (e.getMessage().toLowerCase() - .contains(TestResource.getResource("R_cannotOpenDatabase").toLowerCase())) { - // Only check the timing if the correct error, "cannot open database", is returned. - totalTime = System.currentTimeMillis() - timerStart; - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(expectedMaxTime), "total time: " + totalTime - + ", expected max time: " + TimeUnit.SECONDS.toMillis(expectedMaxTime)); - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(expectedMinTime), "total time: " + totalTime - + ", expected min time: " + TimeUnit.SECONDS.toMillis(expectedMinTime)); - } - } - } - private static void createTable(Statement stmt) throws SQLException { String sql = "create table " + tableName + " (c1 int null);"; stmt.execute(sql); From 193620f1989eb9435d87638509fecbfadcfa739e Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 18 Jun 2024 10:25:59 -0700 Subject: [PATCH 32/57] Missed some parts where connection retry was still included. --- .../microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java | 1 - .../sqlserver/jdbc/ConfigurableRetryLogicTest.java | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 4b00f5135..be56759b6 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -186,7 +186,6 @@ class ConfigRetryRule { private int retryCount = 1; private String retryQueries = ""; private ArrayList waitTimes = new ArrayList<>(); - private boolean replaceExisting = false; public ConfigRetryRule(String rule) throws SQLServerException { addElements(parse(rule)); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 497d23a9a..95de5b34a 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -46,7 +46,7 @@ public static void setupTests() throws Exception { @Test public void testStatementRetryPreparedStatement() throws Exception { connectionStringCRL = TestUtils.addOrOverrideProperty(connectionString, "retryExec", - "{2714:3,2*2:CREATE;2715:1,3;+4060,4070}"); + "{2714:3,2*2:CREATE;2715:1,3}"); try (Connection conn = DriverManager.getConnection(connectionStringCRL); Statement s = conn.createStatement()) { PreparedStatement ps = conn.prepareStatement("create table " + tableName + " (c1 int null);"); @@ -72,7 +72,7 @@ public void testStatementRetryPreparedStatement() throws Exception { @Test public void testStatementRetryCallableStatement() throws Exception { connectionStringCRL = TestUtils.addOrOverrideProperty(connectionString, "retryExec", - "{2714:3,2*2:CREATE;2715:1,3;+4060,4070}"); + "{2714:3,2*2:CREATE;2715:1,3}"); String call = "create table " + tableName + " (c1 int null);"; try (Connection conn = DriverManager.getConnection(connectionStringCRL); Statement s = conn.createStatement(); @@ -236,8 +236,6 @@ public void testCorrectlyFormattedRules() { testStatementRetry("retryExec={;};"); // Test length 1 - testStatementRetry("retryExec={4060};"); - testStatementRetry("retryExec={+4060,4070};"); testStatementRetry("retryExec={2714:1;};"); // Test length 2 From 5e5858fb35ca1ece203d23082c3763e068cae7d6 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 18 Jun 2024 11:08:11 -0700 Subject: [PATCH 33/57] Forgot one more part --- .../microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java index 95de5b34a..3b1d4c665 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java @@ -272,7 +272,7 @@ public void testCorrectlyFormattedRules() { public void testRetryError() throws Exception { // Test incorrect format (NaN) try { - testStatementRetry("retryExec={TEST};"); + testStatementRetry("retryExec={TEST:TEST};"); } catch (SQLServerException e) { assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); } From fdb38d810014df128734e1dce1a0bdb1befa3a4d Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Sun, 11 Aug 2024 15:42:46 -0700 Subject: [PATCH 34/57] Added (most) PR comment revisions. --- .../sqlserver/jdbc/ConfigRetryRule.java | 189 +++++++++++++++ .../jdbc/ConfigurableRetryLogic.java | 221 +++--------------- .../sqlserver/jdbc/ISQLServerDataSource.java | 4 +- .../sqlserver/jdbc/SQLServerConnection.java | 1 - .../sqlserver/jdbc/SQLServerResource.java | 6 +- .../sqlserver/jdbc/SQLServerStatement.java | 20 +- .../ConfigurableRetryLogicTest.java | 20 +- 7 files changed, 254 insertions(+), 207 deletions(-) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java rename src/test/java/com/microsoft/sqlserver/jdbc/{ => configurableretry}/ConfigurableRetryLogicTest.java (92%) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java new file mode 100644 index 000000000..03d056bef --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java @@ -0,0 +1,189 @@ +package com.microsoft.sqlserver.jdbc; + +import java.text.MessageFormat; +import java.util.ArrayList; + + +/** + * The ConfigRetryRule object is what is used by the ConfigurableRetryLogic class to handle statement retries. Each + * ConfigRetryRule object allows for one rule. + * + */ +public class ConfigRetryRule { + private String retryError; + private String operand = "+"; + private final String PLUS_SIGN = "+"; + private final String MULTIPLICATION_SIGN = "*"; + private int initialRetryTime = 0; + private int retryChange = 2; + private int retryCount = 1; + private String retryQueries = ""; + private final String NON_POSITIVE_INT = "Not a positive number"; + private final String TOO_MANY_ARGS = "Too many arguments"; + private final String OPEN_BRACE = "{"; + private final String CLOSING_BRACE = "}"; + private final String COMMA = ","; + private final String COLON = ":"; + private final String ZERO = "0"; + + private ArrayList waitTimes = new ArrayList<>(); + + public ConfigRetryRule(String rule) throws SQLServerException { + addElements(parse(rule)); + calcWaitTime(); + } + + public ConfigRetryRule(String rule, ConfigRetryRule base) { + copyFromExisting(base); + this.retryError = rule; + } + + private void copyFromExisting(ConfigRetryRule base) { + this.retryError = base.getError(); + this.operand = base.getOperand(); + this.initialRetryTime = base.getInitialRetryTime(); + this.retryChange = base.getRetryChange(); + this.retryCount = base.getRetryCount(); + this.retryQueries = base.getRetryQueries(); + this.waitTimes = base.getWaitTimes(); + } + + private String[] parse(String rule) { + if (rule.endsWith(COLON)) { + rule = rule + ZERO; // Add a zero to make below parsing easier + } + + rule = rule.replace(OPEN_BRACE, ""); + rule = rule.replace(CLOSING_BRACE, ""); + rule = rule.trim(); + + return rule.split(COLON); + } + + /** + * Checks if the value passed in is numeric. In the case where the value contains a comma, the value must be a + * multi-error value, e.g. 2714,2716. This must be separated, and each error checked separately. + * + * @param value + * The value to be checked + * @throws SQLServerException + * if a non-numeric value is passed in + */ + private void parameterIsNumeric(String value) throws SQLServerException { + if (!StringUtils.isNumeric(value)) { + String[] arr = value.split(COMMA); + for (String error : arr) { + if (!StringUtils.isNumeric(error)) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); + Object[] msgArgs = {error, NON_POSITIVE_INT}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + } + } + } + + private void addElements(String[] rule) throws SQLServerException { + if (rule.length == 2 || rule.length == 3) { + parameterIsNumeric(rule[0]); + retryError = rule[0]; + String[] timings = rule[1].split(COMMA); + parameterIsNumeric(timings[0]); + int parsedRetryCount = Integer.parseInt(timings[0]); + + if (parsedRetryCount > 0) { + retryCount = parsedRetryCount; + } else { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); + Object[] msgArgs = {parsedRetryCount, NON_POSITIVE_INT}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + + if (timings.length == 2) { + if (timings[1].contains(MULTIPLICATION_SIGN)) { + String[] initialAndChange = timings[1].split("\\*"); + parameterIsNumeric(initialAndChange[0]); + + initialRetryTime = Integer.parseInt(initialAndChange[0]); + operand = MULTIPLICATION_SIGN; + if (initialAndChange.length > 1) { + parameterIsNumeric(initialAndChange[1]); + retryChange = Integer.parseInt(initialAndChange[1]); + } else { + retryChange = initialRetryTime; + } + } else if (timings[1].contains(PLUS_SIGN)) { + String[] initialAndChange = timings[1].split("\\+"); + parameterIsNumeric(initialAndChange[0]); + + initialRetryTime = Integer.parseInt(initialAndChange[0]); + operand = PLUS_SIGN; + if (initialAndChange.length > 1) { + parameterIsNumeric(initialAndChange[1]); + retryChange = Integer.parseInt(initialAndChange[1]); + } else { + retryChange = initialRetryTime; + } + } else { + parameterIsNumeric(timings[1]); + initialRetryTime = Integer.parseInt(timings[1]); + } + } else if (timings.length > 2) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); + Object[] msgArgs = {rule[1], TOO_MANY_ARGS}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + + if (rule.length == 3) { + retryQueries = (rule[2].equals(ZERO) ? "" : rule[2].toLowerCase()); + } + } else { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); + Object[] msgArgs = {rule.length}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + } + + private void calcWaitTime() { + for (int i = 0; i < retryCount; ++i) { + int waitTime = initialRetryTime; + if (operand.equals(PLUS_SIGN)) { + for (int j = 0; j < i; ++j) { + waitTime += retryChange; + } + } else if (operand.equals(MULTIPLICATION_SIGN)) { + for (int k = 0; k < i; ++k) { + waitTime *= retryChange; + } + } + waitTimes.add(waitTime); + } + } + + public String getError() { + return retryError; + } + + public String getOperand() { + return operand; + } + + public int getInitialRetryTime() { + return initialRetryTime; + } + + public int getRetryChange() { + return retryChange; + } + + public int getRetryCount() { + return retryCount; + } + + public String getRetryQueries() { + return retryQueries; + } + + public ArrayList getWaitTimes() { + return waitTimes; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index be56759b6..c19ad9467 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -10,8 +10,6 @@ import java.io.FileReader; import java.io.IOException; import java.net.URI; -import java.text.MessageFormat; -import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -21,8 +19,13 @@ import java.util.concurrent.locks.ReentrantLock; +/** + * Allows configurable statement retry through the use of the 'retryExec' connection property. Each rule read in is + * converted to ConfigRetryRule objects, which are stored and referenced during statement retry. + * + */ public class ConfigurableRetryLogic { - private final static int INTERVAL_BETWEEN_READS = 30000; + private final static int INTERVAL_BETWEEN_READS_IN_MS = 30000; private final static String DEFAULT_PROPS_FILE = "mssql-jdbc.properties"; private static final java.util.logging.Logger CONFIGURABLE_RETRY_LOGGER = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); @@ -33,6 +36,11 @@ public class ConfigurableRetryLogic { private static String rulesFromConnectionString = ""; private static HashMap stmtRules = new HashMap<>(); private static final Lock CRL_LOCK = new ReentrantLock(); + private static final String SEMI_COLON = ";"; + private static final String COMMA = ","; + private static final String FORWARD_SLASH = "/"; + private static final String EQUALS_SIGN = "="; + private static final String RETRY_EXEC = "retryExec"; private ConfigurableRetryLogic() throws SQLServerException { timeLastRead = new Date().getTime(); @@ -48,22 +56,26 @@ private ConfigurableRetryLogic() throws SQLServerException { * an exception */ public static ConfigurableRetryLogic getInstance() throws SQLServerException { - CRL_LOCK.lock(); - try { - if (driverInstance == null) { - driverInstance = new ConfigurableRetryLogic(); - } else { - reread(); + if (driverInstance == null) { + CRL_LOCK.lock(); + try { + if (driverInstance == null) { + driverInstance = new ConfigurableRetryLogic(); + } else { + refreshRuleSet(); + } + } finally { + CRL_LOCK.unlock(); } - return driverInstance; - } finally { - CRL_LOCK.unlock(); + } else { + refreshRuleSet(); } + return driverInstance; } - private static void reread() throws SQLServerException { + private static void refreshRuleSet() throws SQLServerException { long currentTime = new Date().getTime(); - if ((currentTime - timeLastRead) >= INTERVAL_BETWEEN_READS && !compareModified()) { + if ((currentTime - timeLastRead) >= INTERVAL_BETWEEN_READS_IN_MS && !compareModified()) { timeLastRead = currentTime; setUpRules(); } @@ -100,7 +112,7 @@ private static void setUpRules() throws SQLServerException { if (!rulesFromConnectionString.isEmpty()) { temp = new LinkedList<>(); - Collections.addAll(temp, rulesFromConnectionString.split(";")); + Collections.addAll(temp, rulesFromConnectionString.split(SEMI_COLON)); rulesFromConnectionString = ""; } else { temp = readFromFile(); @@ -114,8 +126,8 @@ private static void createRules(LinkedList listOfRules) throws SQLServer for (String potentialRule : listOfRules) { ConfigRetryRule rule = new ConfigRetryRule(potentialRule); - if (rule.getError().contains(",")) { - String[] arr = rule.getError().split(","); + if (rule.getError().contains(COMMA)) { + String[] arr = rule.getError().split(COMMA); for (String retryError : arr) { ConfigRetryRule splitRule = new ConfigRetryRule(retryError, rule); @@ -132,7 +144,7 @@ private static String getCurrentClassPath() { String className = new Object() {}.getClass().getEnclosingClass().getName(); String location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath(); location = location.substring(0, location.length() - 16); - URI uri = new URI(location + "/"); + URI uri = new URI(location + FORWARD_SLASH); return uri.getPath(); } catch (Exception e) { if (CONFIGURABLE_RETRY_LOGGER.isLoggable(java.util.logging.Level.FINEST)) { @@ -152,9 +164,9 @@ private static LinkedList readFromFile() { try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { String readLine; while ((readLine = buffer.readLine()) != null) { - if (readLine.startsWith("retryExec")) { - String value = readLine.split("=")[1]; - Collections.addAll(list, value.split(";")); + if (readLine.startsWith(RETRY_EXEC)) { + String value = readLine.split(EQUALS_SIGN)[1]; + Collections.addAll(list, value.split(SEMI_COLON)); } } } @@ -167,7 +179,7 @@ private static LinkedList readFromFile() { } ConfigRetryRule searchRuleSet(int ruleToSearch) throws SQLServerException { - reread(); + refreshRuleSet(); for (Map.Entry entry : stmtRules.entrySet()) { if (entry.getKey() == ruleToSearch) { return entry.getValue(); @@ -176,168 +188,3 @@ ConfigRetryRule searchRuleSet(int ruleToSearch) throws SQLServerException { return null; } } - - -class ConfigRetryRule { - private String retryError; - private String operand = "+"; - private int initialRetryTime = 0; - private int retryChange = 2; - private int retryCount = 1; - private String retryQueries = ""; - private ArrayList waitTimes = new ArrayList<>(); - - public ConfigRetryRule(String rule) throws SQLServerException { - addElements(parse(rule)); - calcWaitTime(); - } - - public ConfigRetryRule(String rule, ConfigRetryRule base) { - copyFromExisting(base); - this.retryError = rule; - } - - private void copyFromExisting(ConfigRetryRule base) { - this.retryError = base.getError(); - this.operand = base.getOperand(); - this.initialRetryTime = base.getInitialRetryTime(); - this.retryChange = base.getRetryChange(); - this.retryCount = base.getRetryCount(); - this.retryQueries = base.getRetryQueries(); - this.waitTimes = base.getWaitTimes(); - } - - private String[] parse(String rule) { - String parsed = rule + " "; - - parsed = parsed.replace(": ", ":0"); - parsed = parsed.replace("{", ""); - parsed = parsed.replace("}", ""); - parsed = parsed.trim(); - - return parsed.split(":"); - } - - private void parameterIsNumeric(String value) throws SQLServerException { - if (!StringUtils.isNumeric(value)) { - String[] arr = value.split(","); - for (String error : arr) { - if (!StringUtils.isNumeric(error)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); - throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); - } - } - } - } - - private void addElements(String[] rule) throws SQLServerException { - if (rule.length == 2 || rule.length == 3) { - parameterIsNumeric(rule[0]); - retryError = rule[0]; - String[] timings = rule[1].split(","); - parameterIsNumeric(timings[0]); - - if (Integer.parseInt(timings[0]) > 0) { - retryCount = Integer.parseInt(timings[0]); - } else { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); - throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); - } - - if (timings.length == 2) { - if (timings[1].contains("*")) { - String[] initialAndChange = timings[1].split("\\*"); - parameterIsNumeric(initialAndChange[0]); - - initialRetryTime = Integer.parseInt(initialAndChange[0]); - operand = "*"; - if (initialAndChange.length > 1) { - parameterIsNumeric(initialAndChange[1]); - retryChange = Integer.parseInt(initialAndChange[1]); - } else { - retryChange = initialRetryTime; - } - } else if (timings[1].contains("+")) { - String[] initialAndChange = timings[1].split("\\+"); - parameterIsNumeric(initialAndChange[0]); - - initialRetryTime = Integer.parseInt(initialAndChange[0]); - operand = "+"; - if (initialAndChange.length > 1) { - parameterIsNumeric(initialAndChange[1]); - retryChange = Integer.parseInt(initialAndChange[1]); - } else { - retryChange = initialRetryTime; - } - } else { - parameterIsNumeric(timings[1]); - initialRetryTime = Integer.parseInt(timings[1]); - } - } else if (timings.length > 2) { - StringBuilder builder = new StringBuilder(); - for (String string : rule) { - builder.append(string); - } - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); - Object[] msgArgs = {builder.toString()}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, true); - } - - if (rule.length == 3) { - retryQueries = (rule[2].equals("0") ? "" : rule[2].toLowerCase()); - } - } else { - StringBuilder builder = new StringBuilder(); - for (String string : rule) { - builder.append(string); - } - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidRuleFormat")); - Object[] msgArgs = {builder.toString()}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, true); - } - } - - private void calcWaitTime() { - for (int i = 0; i < retryCount; ++i) { - int waitTime = initialRetryTime; - if (operand.equals("+")) { - for (int j = 0; j < i; ++j) { - waitTime += retryChange; - } - } else if (operand.equals("*")) { - for (int k = 0; k < i; ++k) { - waitTime *= retryChange; - } - } - waitTimes.add(waitTime); - } - } - - public String getError() { - return retryError; - } - - public String getOperand() { - return operand; - } - - public int getInitialRetryTime() { - return initialRetryTime; - } - - public int getRetryChange() { - return retryChange; - } - - public int getRetryCount() { - return retryCount; - } - - public String getRetryQueries() { - return retryQueries; - } - - public ArrayList getWaitTimes() { - return waitTimes; - } -} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java index 4211c225d..514156346 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java @@ -1355,13 +1355,15 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { * Returns value of 'retryExec' from Connection String. * * @param retryExec + * Set of rules used for statement (execution) retry */ void setRetryExec(String retryExec); /** * Sets the value for 'retryExec' property * - * @return retryExec String value + * @return retryExec + * String value */ String getRetryExec(); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 34d693087..51c09aab2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -2016,7 +2016,6 @@ Connection connect(Properties propsIn, SQLServerPooledConnection pooledConnectio throw e; } else { // only retry if transient error - SQLServerError sqlServerError = e.getSQLServerError(); if (!TransientError.isTransientError(sqlServerError)) { throw e; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 85ec1cfeb..34f853892 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -548,9 +548,9 @@ protected Object[][] getContents() { {"R_InvalidSqlQuery", "Invalid SQL Query: {0}"}, {"R_InvalidScale", "Scale of input value is larger than the maximum allowed by SQL Server."}, {"R_colCountNotMatchColTypeCount", "Number of provided columns {0} does not match the column data types definition {1}."}, - {"R_InvalidParameterFormat", "One or more supplied parameters are not correct."}, - {"R_InvalidRuleFormat", "Wrong number of parameters supplied to rule."}, - {"R_InvalidRetryInterval", "Current retry interval is longer than queryTimeout."}, + {"R_InvalidParameterFormat", "One or more supplied parameters is/are not correct: {0}, for the reason: {1}."}, + {"R_InvalidRuleFormat", "Wrong number of parameters supplied to rule. Number of parameters: {0}, expected: 2 or 3."}, + {"R_InvalidRetryInterval", "Current retry interval: {0}, is longer than queryTimeout: {1}."}, }; } // @formatter:on diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index dc3ca41b9..e0aaa0c39 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -275,22 +275,28 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ rule = ConfigurableRetryLogic.getInstance().searchRuleSet(e.getSQLServerError().getErrorNumber()); } + // If there is a rule for this error AND we still have retries remaining THEN we can proceed, otherwise + // first check for query timeout, and then throw the error if queryTimeout was not reached if (null != rule && retryAttempt < rule.getRetryCount()) { - boolean meetsQueryMatch = true; + + // Also check if the last executed statement matches the query constraint passed in for the rule. + // Defaults to true, changed to false if the query does NOT match. + boolean matchesDefinedQuery = true; if (!(rule.getRetryQueries().isEmpty())) { - // If query has been defined for the rule, we need to query match - meetsQueryMatch = rule.getRetryQueries() + + matchesDefinedQuery = rule.getRetryQueries() .contains(ConfigurableRetryLogic.getInstance().getLastQuery().split(" ")[0]); } - if (meetsQueryMatch) { + if (matchesDefinedQuery) { try { int timeToWait = rule.getWaitTimes().get(retryAttempt); - if (connection.getQueryTimeoutSeconds() >= 0 - && timeToWait > connection.getQueryTimeoutSeconds()) { + int queryTimeout = connection.getQueryTimeoutSeconds(); + if (queryTimeout >= 0 && timeToWait > queryTimeout) { MessageFormat form = new MessageFormat( SQLServerException.getErrString("R_InvalidRetryInterval")); - throw new SQLServerException(null, form.format(new Object[] {}), null, 0, true); + Object[] msgArgs = {timeToWait, queryTimeout}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); } try { Thread.sleep(TimeUnit.SECONDS.toMillis(timeToWait)); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java similarity index 92% rename from src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java rename to src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index 3b1d4c665..cd4f31411 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -3,7 +3,7 @@ * available under the terms of the MIT License. See the LICENSE file in the project root for more information. */ -package com.microsoft.sqlserver.jdbc; +package com.microsoft.sqlserver.jdbc.configurableretry; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -20,6 +20,10 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import com.microsoft.sqlserver.jdbc.RandomUtil; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.TestResource; +import com.microsoft.sqlserver.jdbc.TestUtils; import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; import com.microsoft.sqlserver.testframework.AbstractTest; @@ -53,7 +57,7 @@ public void testStatementRetryPreparedStatement() throws Exception { try { createTable(s); ps.execute(); - fail(TestResource.getResource("R_expectedFailPassed")); + Assertions.fail(TestResource.getResource("R_expectedFailPassed")); } catch (SQLServerException e) { assertTrue(e.getMessage().startsWith("There is already an object"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + e.getMessage()); @@ -165,8 +169,8 @@ public void statementTimingTests() { totalTime = System.currentTimeMillis() - timerStart; assertTrue(totalTime > TimeUnit.SECONDS.toMillis(5), "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(5)); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(15), "total time: " + totalTime - + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(15)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(15), + "total time: " + totalTime + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(15)); } timerStart = System.currentTimeMillis(); @@ -180,8 +184,8 @@ public void statementTimingTests() { totalTime = System.currentTimeMillis() - timerStart; assertTrue(totalTime > TimeUnit.SECONDS.toMillis(8), "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(8)); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(18), "total time: " + totalTime - + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(18)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(18), + "total time: " + totalTime + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(18)); } timerStart = System.currentTimeMillis(); @@ -195,8 +199,8 @@ public void statementTimingTests() { totalTime = System.currentTimeMillis() - timerStart; assertTrue(totalTime > TimeUnit.SECONDS.toMillis(10), "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(10)); - assertTrue(totalTime < TimeUnit.SECONDS.toMillis(20), "total time: " + totalTime - + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(20)); + assertTrue(totalTime < TimeUnit.SECONDS.toMillis(20), + "total time: " + totalTime + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(20)); } } From 6465a957e24c76e4f0fa24677d93c8307ae506b4 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Sun, 11 Aug 2024 16:05:49 -0700 Subject: [PATCH 35/57] Add comments for specified and public facing methods --- .../sqlserver/jdbc/ConfigRetryRule.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java index 03d056bef..a3c05ba4a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java @@ -28,11 +28,29 @@ public class ConfigRetryRule { private ArrayList waitTimes = new ArrayList<>(); + /** + * Default constructor + * + * @param rule + * The rule used to construct the ConfigRetryRule object + * @throws SQLServerException + * If there is a problem parsing the rule + */ public ConfigRetryRule(String rule) throws SQLServerException { addElements(parse(rule)); calcWaitTime(); } + /** + * Allows constructing a ConfigRetryRule object from another ConfigRetryRule object. Used when the first object has + * multiple errors provided. We pass in the multi-error object and create 1 new object for each error in the initial + * object. + * + * @param rule + * The rule used to construct the ConfigRetryRule object + * @param base + * The ConfigRetryRule object to base the new objects off of + */ public ConfigRetryRule(String rule, ConfigRetryRule base) { copyFromExisting(base); this.retryError = rule; @@ -82,6 +100,39 @@ private void parameterIsNumeric(String value) throws SQLServerException { } } + /** + * Parses the passed in string array, containing all elements from the orignal rule, and assigns the information + * to the class variables. The logic is as follows: + *

+ * The rule array, which was created by splitting the rule string based on ":", must be of length 2 or 3. If not + * there are too many parts, and an error is thrown. + *

+ * If it is of length 2 or 3, the first part is always the retry error (the error to retry on). We check if its + * numeric, and if so, assign it to the class variable. The second part are the retry timings, which include + * retry count (mandatory), initial retry time (optional), operand (optional), and retry change (optional). A + * parameter can only be included, if ALL parameters prior to it are included. Thus, these are the only valid rule + * formats: + * error; count + * error; count, initial retry time + * error; count, initial retry time [OPERAND] + * error; count, initial retry time [OPERAND] retry change + *

+ * Next, the second part of the rule is parsed based on "," and each part checked. The retry count is mandatory + * and must be numeric and greater than 0, else an error is thrown. + *

+ * If there is a second part to the retry timings, it includes any of the parameters mentioned above: initial retry + * time, operand, and retry change. We first check if there is an operand, if not, then only initial retry time has + * been given, and it is assigned. If there is an operand, we split this second part based on the operand. + * Whatever was before the operand was the initial retry time, and if there was something after the operand, this + * is the retry change. If there are more than 2 parts to the timing, i.e. more than 2 commas, throw an error. + *

+ * Finally, if the rule has 3 parts, it includes a query specifier, parse this and assign it. + * + * @param rule + * The passed in rule, as a string array + * @throws SQLServerException + * If a rule or parameter has invalid inputs + */ private void addElements(String[] rule) throws SQLServerException { if (rule.length == 2 || rule.length == 3) { parameterIsNumeric(rule[0]); @@ -159,30 +210,72 @@ private void calcWaitTime() { } } + /** + * Returns the retry error for this ConfigRetryRule object. + * + * @return + * The retry error + */ public String getError() { return retryError; } + /** + * Returns the retry error for this ConfigRetryRule object. + * + * @return + * The retry error + */ public String getOperand() { return operand; } + /** + * Returns the retry error (errors to retry on) for this ConfigRetryRule object. + * + * @return + * The retry error + */ public int getInitialRetryTime() { return initialRetryTime; } + /** + * Returns the retry change (timing change to apply to wait times) for this ConfigRetryRule object. + * + * @return + * The retry change + */ public int getRetryChange() { return retryChange; } + /** + * Returns the retry count (amount of times to retry) for this ConfigRetryRule object. + * + * @return + * The retry count + */ public int getRetryCount() { return retryCount; } + /** + * Returns the retry query specifier for this ConfigRetryRule object. + * + * @return + * The retry query specifier + */ public String getRetryQueries() { return retryQueries; } + /** + * Returns an array listing the waiting times between each retry, for this ConfigRetryRule object. + * + * @return + * The list of waiting times + */ public ArrayList getWaitTimes() { return waitTimes; } From 46951f14f7392501dc6cce9124b92e44d3aff89a Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 12 Aug 2024 09:44:41 -0700 Subject: [PATCH 36/57] Added a missing test --- .../sqlserver/jdbc/ConfigRetryRule.java | 2 +- .../ConfigurableRetryLogicTest.java | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java index a3c05ba4a..8b3f13030 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java @@ -111,7 +111,7 @@ private void parameterIsNumeric(String value) throws SQLServerException { * numeric, and if so, assign it to the class variable. The second part are the retry timings, which include * retry count (mandatory), initial retry time (optional), operand (optional), and retry change (optional). A * parameter can only be included, if ALL parameters prior to it are included. Thus, these are the only valid rule - * formats: + * formats for rules of length 2: * error; count * error; count, initial retry time * error; count, initial retry time [OPERAND] diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index cd4f31411..f200beb47 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -16,14 +16,11 @@ import java.sql.Statement; import java.util.concurrent.TimeUnit; +import com.microsoft.sqlserver.jdbc.*; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import com.microsoft.sqlserver.jdbc.RandomUtil; -import com.microsoft.sqlserver.jdbc.SQLServerException; -import com.microsoft.sqlserver.jdbc.TestResource; -import com.microsoft.sqlserver.jdbc.TestUtils; import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; import com.microsoft.sqlserver.testframework.AbstractTest; @@ -41,6 +38,27 @@ public static void setupTests() throws Exception { setConnection(); } + @Test + public void testRetryExecConnectionStringOption() throws Exception { + try (SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionString); + Statement s = conn.createStatement()) { + String test = conn.getRetryExec(); + assertTrue(test.isEmpty()); + conn.setRetryExec("{2714:3,2*2:CREATE;2715:1,3}"); + PreparedStatement ps = conn.prepareStatement("create table " + tableName + " (c1 int null);"); + try { + createTable(s); + ps.execute(); + Assertions.fail(TestResource.getResource("R_expectedFailPassed")); + } catch (SQLServerException e) { + assertTrue(e.getMessage().startsWith("There is already an object"), + TestResource.getResource("R_unexpectedExceptionContent") + ": " + e.getMessage()); + } finally { + dropTable(s); + } + } + } + /** * Tests that statement retry works with prepared statements. * From 0be2a3b3e23e6c8dd4375a60b81411e3814e8e66 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Mon, 12 Aug 2024 11:45:18 -0700 Subject: [PATCH 37/57] More tests --- .../com/microsoft/sqlserver/jdbc/ConfigRetryRule.java | 10 +--------- .../configurableretry/ConfigurableRetryLogicTest.java | 3 +++ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java index 8b3f13030..e48ec284b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java @@ -139,15 +139,7 @@ private void addElements(String[] rule) throws SQLServerException { retryError = rule[0]; String[] timings = rule[1].split(COMMA); parameterIsNumeric(timings[0]); - int parsedRetryCount = Integer.parseInt(timings[0]); - - if (parsedRetryCount > 0) { - retryCount = parsedRetryCount; - } else { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); - Object[] msgArgs = {parsedRetryCount, NON_POSITIVE_INT}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, true); - } + retryCount = Integer.parseInt(timings[0]); if (timings.length == 2) { if (timings[1].contains(MULTIPLICATION_SIGN)) { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index f200beb47..676fdc83e 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -263,6 +263,9 @@ public void testCorrectlyFormattedRules() { // Test length 2 testStatementRetry("retryExec={2714:1,3;};"); + // Test length 3, but query is empty + testStatementRetry("retryExec={2714:1,3:;};"); + // Test length 3, also multiple statement errors testStatementRetry("retryExec={2714,2716:1,2*2:CREATE};"); From 5d45f4d065f4e09f710413561ce1e551390ba408 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 13 Aug 2024 09:48:12 -0700 Subject: [PATCH 38/57] Added more missing tests --- .../sqlserver/jdbc/ConfigRetryRule.java | 45 ++++++++++--------- .../jdbc/ConfigurableRetryLogic.java | 28 ++++++++---- .../ConfigurableRetryLogicTest.java | 44 +++++++++++++++++- 3 files changed, 87 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java index e48ec284b..dbe08adf0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java @@ -32,9 +32,9 @@ public class ConfigRetryRule { * Default constructor * * @param rule - * The rule used to construct the ConfigRetryRule object + * The rule used to construct the ConfigRetryRule object * @throws SQLServerException - * If there is a problem parsing the rule + * If there is a problem parsing the rule */ public ConfigRetryRule(String rule) throws SQLServerException { addElements(parse(rule)); @@ -47,9 +47,9 @@ public ConfigRetryRule(String rule) throws SQLServerException { * object. * * @param rule - * The rule used to construct the ConfigRetryRule object + * The rule used to construct the ConfigRetryRule object * @param base - * The ConfigRetryRule object to base the new objects off of + * The ConfigRetryRule object to base the new objects off of */ public ConfigRetryRule(String rule, ConfigRetryRule base) { copyFromExisting(base); @@ -101,12 +101,14 @@ private void parameterIsNumeric(String value) throws SQLServerException { } /** - * Parses the passed in string array, containing all elements from the orignal rule, and assigns the information + * Parses the passed in string array, containing all elements from the original rule, and assigns the information * to the class variables. The logic is as follows: - *

+ *

+ *

* The rule array, which was created by splitting the rule string based on ":", must be of length 2 or 3. If not * there are too many parts, and an error is thrown. - *

+ *

+ *

* If it is of length 2 or 3, the first part is always the retry error (the error to retry on). We check if its * numeric, and if so, assign it to the class variable. The second part are the retry timings, which include * retry count (mandatory), initial retry time (optional), operand (optional), and retry change (optional). A @@ -116,22 +118,25 @@ private void parameterIsNumeric(String value) throws SQLServerException { * error; count, initial retry time * error; count, initial retry time [OPERAND] * error; count, initial retry time [OPERAND] retry change - *

+ *

+ *

* Next, the second part of the rule is parsed based on "," and each part checked. The retry count is mandatory * and must be numeric and greater than 0, else an error is thrown. - *

+ *

+ *

* If there is a second part to the retry timings, it includes any of the parameters mentioned above: initial retry * time, operand, and retry change. We first check if there is an operand, if not, then only initial retry time has * been given, and it is assigned. If there is an operand, we split this second part based on the operand. * Whatever was before the operand was the initial retry time, and if there was something after the operand, this * is the retry change. If there are more than 2 parts to the timing, i.e. more than 2 commas, throw an error. - *

+ *

+ *

* Finally, if the rule has 3 parts, it includes a query specifier, parse this and assign it. * * @param rule - * The passed in rule, as a string array + * The passed in rule, as a string array * @throws SQLServerException - * If a rule or parameter has invalid inputs + * If a rule or parameter has invalid inputs */ private void addElements(String[] rule) throws SQLServerException { if (rule.length == 2 || rule.length == 3) { @@ -164,7 +169,7 @@ private void addElements(String[] rule) throws SQLServerException { parameterIsNumeric(initialAndChange[1]); retryChange = Integer.parseInt(initialAndChange[1]); } else { - retryChange = initialRetryTime; + retryChange = 2; } } else { parameterIsNumeric(timings[1]); @@ -206,7 +211,7 @@ private void calcWaitTime() { * Returns the retry error for this ConfigRetryRule object. * * @return - * The retry error + * The retry error */ public String getError() { return retryError; @@ -216,7 +221,7 @@ public String getError() { * Returns the retry error for this ConfigRetryRule object. * * @return - * The retry error + * The retry error */ public String getOperand() { return operand; @@ -226,7 +231,7 @@ public String getOperand() { * Returns the retry error (errors to retry on) for this ConfigRetryRule object. * * @return - * The retry error + * The retry error */ public int getInitialRetryTime() { return initialRetryTime; @@ -236,7 +241,7 @@ public int getInitialRetryTime() { * Returns the retry change (timing change to apply to wait times) for this ConfigRetryRule object. * * @return - * The retry change + * The retry change */ public int getRetryChange() { return retryChange; @@ -246,7 +251,7 @@ public int getRetryChange() { * Returns the retry count (amount of times to retry) for this ConfigRetryRule object. * * @return - * The retry count + * The retry count */ public int getRetryCount() { return retryCount; @@ -256,7 +261,7 @@ public int getRetryCount() { * Returns the retry query specifier for this ConfigRetryRule object. * * @return - * The retry query specifier + * The retry query specifier */ public String getRetryQueries() { return retryQueries; @@ -266,7 +271,7 @@ public String getRetryQueries() { * Returns an array listing the waiting times between each retry, for this ConfigRetryRule object. * * @return - * The list of waiting times + * The list of waiting times */ public ArrayList getWaitTimes() { return waitTimes; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index c19ad9467..0c2ea17b2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -33,7 +33,8 @@ public class ConfigurableRetryLogic { private static long timeLastModified; private static long timeLastRead; private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) - private static String rulesFromConnectionString = ""; + private static String newRulesFromConnectionString = ""; // Are their new rules to read? + private static String lastRulesFromConnectionString = ""; // Have we read from conn string in the past? private static HashMap stmtRules = new HashMap<>(); private static final Lock CRL_LOCK = new ReentrantLock(); private static final String SEMI_COLON = ";"; @@ -73,27 +74,35 @@ public static ConfigurableRetryLogic getInstance() throws SQLServerException { return driverInstance; } + /** + * If it has been INTERVAL_BETWEEN_READS_IN_MS (30 secs) since last read and EITHER, we're using connection string + * props OR the file contents have been updated, reread. + * + * @throws SQLServerException + * when an exception occurs + */ private static void refreshRuleSet() throws SQLServerException { long currentTime = new Date().getTime(); - if ((currentTime - timeLastRead) >= INTERVAL_BETWEEN_READS_IN_MS && !compareModified()) { + if ((currentTime - timeLastRead) >= INTERVAL_BETWEEN_READS_IN_MS + && ((!lastRulesFromConnectionString.isEmpty()) || rulesHaveBeenChanged())) { timeLastRead = currentTime; setUpRules(); } } - private static boolean compareModified() { + private static boolean rulesHaveBeenChanged() { String inputToUse = getCurrentClassPath() + DEFAULT_PROPS_FILE; try { File f = new File(inputToUse); - return f.lastModified() == timeLastModified; + return f.lastModified() != timeLastModified; } catch (Exception e) { - return false; + return true; } } void setFromConnectionString(String custom) throws SQLServerException { - rulesFromConnectionString = custom; + newRulesFromConnectionString = custom; setUpRules(); } @@ -110,10 +119,11 @@ private static void setUpRules() throws SQLServerException { lastQuery = ""; LinkedList temp; - if (!rulesFromConnectionString.isEmpty()) { + if (!newRulesFromConnectionString.isEmpty()) { temp = new LinkedList<>(); - Collections.addAll(temp, rulesFromConnectionString.split(SEMI_COLON)); - rulesFromConnectionString = ""; + Collections.addAll(temp, newRulesFromConnectionString.split(SEMI_COLON)); + lastRulesFromConnectionString = newRulesFromConnectionString; + newRulesFromConnectionString = ""; } else { temp = readFromFile(); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index 676fdc83e..d0926d258 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -16,11 +16,15 @@ import java.sql.Statement; import java.util.concurrent.TimeUnit; -import com.microsoft.sqlserver.jdbc.*; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import com.microsoft.sqlserver.jdbc.RandomUtil; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.TestResource; +import com.microsoft.sqlserver.jdbc.TestUtils; import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; import com.microsoft.sqlserver.testframework.AbstractTest; @@ -38,6 +42,12 @@ public static void setupTests() throws Exception { setConnection(); } + /** + * Test that the SQLServerConnection methods getRetryExec and setRetryExec are working. + * + * @throws Exception + * if an exception occurs + */ @Test public void testRetryExecConnectionStringOption() throws Exception { try (SQLServerConnection conn = (SQLServerConnection) DriverManager.getConnection(connectionString); @@ -246,6 +256,20 @@ public void readFromFile() { } } + /** + * Ensure that CRL properly re-reads rules after INTERVAL_BETWEEN_READS_IN_MS (30 secs). + */ + @Test + public void rereadAfterInterval() { + try { + testStatementRetry("retryExec={2716:1,2*2:CREATE;};"); + Thread.sleep(30000); // Sleep to ensure it has been INTERVAL_BETWEEN_READS_IN_MS between reads + testStatementRetry("retryExec={2714:1,2*2:CREATE;};"); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } + } + /** * Tests that rules of the correct length, and containing valid values, pass */ @@ -263,6 +287,10 @@ public void testCorrectlyFormattedRules() { // Test length 2 testStatementRetry("retryExec={2714:1,3;};"); + // Test length 2, with operand, but no initial-retry-time + testStatementRetry("retryExec={2714:1,3+;};"); + testStatementRetry("retryExec={2714:1,3*;};"); + // Test length 3, but query is empty testStatementRetry("retryExec={2714:1,3:;};"); @@ -287,6 +315,20 @@ public void testCorrectlyFormattedRules() { } } + /** + * Tests that too many timing parameters (>2) causes InvalidParameterFormat Exception. + */ + @Test + public void testTooManyTimings() { + try { + testStatementRetry("retryExec={2714,2716:1,2*2,1:CREATE};"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + } catch (Exception e) { + Assertions.fail(TestResource.getResource("R_unexpectedException")); + } + } + /** * Tests that rules with an invalid retry error correctly fail. * From cb24aa82a34db93765f5006e14dbecc630eedbce Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 13 Aug 2024 11:57:11 -0700 Subject: [PATCH 39/57] Resolve retryCount test failure --- .../jdbc/ConfigurableRetryLogic.java | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 0c2ea17b2..03f470822 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -33,8 +33,7 @@ public class ConfigurableRetryLogic { private static long timeLastModified; private static long timeLastRead; private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) - private static String newRulesFromConnectionString = ""; // Are their new rules to read? - private static String lastRulesFromConnectionString = ""; // Have we read from conn string in the past? + private static String prevRulesFromConnectionString = ""; private static HashMap stmtRules = new HashMap<>(); private static final Lock CRL_LOCK = new ReentrantLock(); private static final String SEMI_COLON = ";"; @@ -45,7 +44,7 @@ public class ConfigurableRetryLogic { private ConfigurableRetryLogic() throws SQLServerException { timeLastRead = new Date().getTime(); - setUpRules(); + setUpRules(null); } /** @@ -75,18 +74,23 @@ public static ConfigurableRetryLogic getInstance() throws SQLServerException { } /** - * If it has been INTERVAL_BETWEEN_READS_IN_MS (30 secs) since last read and EITHER, we're using connection string - * props OR the file contents have been updated, reread. + * If it has been INTERVAL_BETWEEN_READS_IN_MS (30 secs) since last read, see if we last did a file read, if so + * only reread if the file has been modified. If no file read, set up rules using the prev. connection string rules. * * @throws SQLServerException * when an exception occurs */ private static void refreshRuleSet() throws SQLServerException { long currentTime = new Date().getTime(); - if ((currentTime - timeLastRead) >= INTERVAL_BETWEEN_READS_IN_MS - && ((!lastRulesFromConnectionString.isEmpty()) || rulesHaveBeenChanged())) { + if ((currentTime - timeLastRead) >= INTERVAL_BETWEEN_READS_IN_MS) { + // If it has been 30 secs, reread timeLastRead = currentTime; - setUpRules(); + if (timeLastModified != 0 && rulesHaveBeenChanged()) { + // If timeLastModified has been set, we have previously read from a file + setUpRules(null); + } else { + setUpRules(prevRulesFromConnectionString); + } } } @@ -102,8 +106,8 @@ private static boolean rulesHaveBeenChanged() { } void setFromConnectionString(String custom) throws SQLServerException { - newRulesFromConnectionString = custom; - setUpRules(); + prevRulesFromConnectionString = custom; + setUpRules(prevRulesFromConnectionString); } void storeLastQuery(String sql) { @@ -114,18 +118,24 @@ String getLastQuery() { return lastQuery; } - private static void setUpRules() throws SQLServerException { + /** + * Sets up rules based on either connection string option or file read. + * + * @param cxnStrRules + * If null, rules are constructed from file, else, this parameter is used to construct rules + * @throws SQLServerException + * If an exception occurs + */ + private static void setUpRules(String cxnStrRules) throws SQLServerException { stmtRules = new HashMap<>(); lastQuery = ""; LinkedList temp; - if (!newRulesFromConnectionString.isEmpty()) { - temp = new LinkedList<>(); - Collections.addAll(temp, newRulesFromConnectionString.split(SEMI_COLON)); - lastRulesFromConnectionString = newRulesFromConnectionString; - newRulesFromConnectionString = ""; - } else { + if (cxnStrRules == null || cxnStrRules.isEmpty()) { temp = readFromFile(); + } else { + temp = new LinkedList<>(); + Collections.addAll(temp, cxnStrRules.split(SEMI_COLON)); } createRules(temp); } @@ -170,7 +180,6 @@ private static LinkedList readFromFile() { try { File f = new File(filePath + DEFAULT_PROPS_FILE); - timeLastModified = f.lastModified(); try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { String readLine; while ((readLine = buffer.readLine()) != null) { @@ -180,6 +189,7 @@ private static LinkedList readFromFile() { } } } + timeLastModified = f.lastModified(); } catch (IOException e) { if (CONFIGURABLE_RETRY_LOGGER.isLoggable(java.util.logging.Level.FINEST)) { CONFIGURABLE_RETRY_LOGGER.finest("No properties file exists or file is badly formatted."); From 7634c39a45ae8a7b8d4fc97c776e3d20d4cd19c5 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 13 Aug 2024 12:59:33 -0700 Subject: [PATCH 40/57] Remove eaten exceptions --- .../jdbc/ConfigurableRetryLogic.java | 43 +++++++++++-------- .../sqlserver/jdbc/SQLServerResource.java | 2 + 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 03f470822..6a356b62d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -5,11 +5,10 @@ package com.microsoft.sqlserver.jdbc; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; +import java.io.*; import java.net.URI; +import java.net.URISyntaxException; +import java.text.MessageFormat; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -27,8 +26,6 @@ public class ConfigurableRetryLogic { private final static int INTERVAL_BETWEEN_READS_IN_MS = 30000; private final static String DEFAULT_PROPS_FILE = "mssql-jdbc.properties"; - private static final java.util.logging.Logger CONFIGURABLE_RETRY_LOGGER = java.util.logging.Logger - .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); private static ConfigurableRetryLogic driverInstance = null; private static long timeLastModified; private static long timeLastRead; @@ -94,7 +91,7 @@ private static void refreshRuleSet() throws SQLServerException { } } - private static boolean rulesHaveBeenChanged() { + private static boolean rulesHaveBeenChanged() throws SQLServerException { String inputToUse = getCurrentClassPath() + DEFAULT_PROPS_FILE; try { @@ -159,22 +156,28 @@ private static void createRules(LinkedList listOfRules) throws SQLServer } } - private static String getCurrentClassPath() { + private static String getCurrentClassPath() throws SQLServerException { + String location = ""; + String className = ""; + try { - String className = new Object() {}.getClass().getEnclosingClass().getName(); - String location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath(); + className = new Object() {}.getClass().getEnclosingClass().getName(); + location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath(); location = location.substring(0, location.length() - 16); URI uri = new URI(location + FORWARD_SLASH); return uri.getPath(); - } catch (Exception e) { - if (CONFIGURABLE_RETRY_LOGGER.isLoggable(java.util.logging.Level.FINEST)) { - CONFIGURABLE_RETRY_LOGGER.finest("Unable to get current class path for properties file reading."); - } + } catch (URISyntaxException e) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVURLInvalid")); + Object[] msgArgs = {location + FORWARD_SLASH}; + throw new SQLServerException(form.format(msgArgs), null, 0, e); + } catch (ClassNotFoundException e) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UnableToFindClass")); + Object[] msgArgs = {className}; + throw new SQLServerException(form.format(msgArgs), null, 0, e); } - return null; } - private static LinkedList readFromFile() { + private static LinkedList readFromFile() throws SQLServerException { String filePath = getCurrentClassPath(); LinkedList list = new LinkedList<>(); @@ -190,10 +193,12 @@ private static LinkedList readFromFile() { } } timeLastModified = f.lastModified(); + } catch (FileNotFoundException e) { + throw new SQLServerException(SQLServerException.getErrString("R_PropertiesFileNotFound"), null, 0, null); } catch (IOException e) { - if (CONFIGURABLE_RETRY_LOGGER.isLoggable(java.util.logging.Level.FINEST)) { - CONFIGURABLE_RETRY_LOGGER.finest("No properties file exists or file is badly formatted."); - } + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream")); + Object[] msgArgs = {e.toString()}; + throw new SQLServerException(form.format(msgArgs), null, 0, e); } return list; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 076343e5a..9add1ae15 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -552,6 +552,8 @@ protected Object[][] getContents() { {"R_InvalidParameterFormat", "One or more supplied parameters is/are not correct: {0}, for the reason: {1}."}, {"R_InvalidRuleFormat", "Wrong number of parameters supplied to rule. Number of parameters: {0}, expected: 2 or 3."}, {"R_InvalidRetryInterval", "Current retry interval: {0}, is longer than queryTimeout: {1}."}, + {"R_UnableToFindClass", "Unable to locate specified class: {0}"}, + {"R_PropertiesFileNotFound", "System cannot find the properties file at the specified path. Verify that the path is correct and you have proper permissions to access it."}, }; } // @formatter:on From 57fac740a4c2606fe95d3876870d88a9c323fccd Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 13 Aug 2024 13:19:02 -0700 Subject: [PATCH 41/57] Removed the file not found exception as we read for file in all cases, not just when using CRL --- .../microsoft/sqlserver/jdbc/ConfigRetryRule.java | 5 +++++ .../sqlserver/jdbc/ConfigurableRetryLogic.java | 14 ++++++++++++-- .../sqlserver/jdbc/SQLServerResource.java | 1 - 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java index dbe08adf0..c2e407d4c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java @@ -1,3 +1,8 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + package com.microsoft.sqlserver.jdbc; import java.text.MessageFormat; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 6a356b62d..1bba660f9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -5,7 +5,11 @@ package com.microsoft.sqlserver.jdbc; -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.text.MessageFormat; @@ -26,6 +30,8 @@ public class ConfigurableRetryLogic { private final static int INTERVAL_BETWEEN_READS_IN_MS = 30000; private final static String DEFAULT_PROPS_FILE = "mssql-jdbc.properties"; + private static final java.util.logging.Logger CONFIGURABLE_RETRY_LOGGER = java.util.logging.Logger + .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); private static ConfigurableRetryLogic driverInstance = null; private static long timeLastModified; private static long timeLastRead; @@ -194,7 +200,11 @@ private static LinkedList readFromFile() throws SQLServerException { } timeLastModified = f.lastModified(); } catch (FileNotFoundException e) { - throw new SQLServerException(SQLServerException.getErrString("R_PropertiesFileNotFound"), null, 0, null); + // If the file is not found either A) We're not using CRL OR B) the path is wrong. Do not error out, instead + // log a message. + if (CONFIGURABLE_RETRY_LOGGER.isLoggable(java.util.logging.Level.FINER)) { + CONFIGURABLE_RETRY_LOGGER.finest("No properties file exists or the file path is incorrect."); + } } catch (IOException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream")); Object[] msgArgs = {e.toString()}; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 9add1ae15..2f2eb92e2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -553,7 +553,6 @@ protected Object[][] getContents() { {"R_InvalidRuleFormat", "Wrong number of parameters supplied to rule. Number of parameters: {0}, expected: 2 or 3."}, {"R_InvalidRetryInterval", "Current retry interval: {0}, is longer than queryTimeout: {1}."}, {"R_UnableToFindClass", "Unable to locate specified class: {0}"}, - {"R_PropertiesFileNotFound", "System cannot find the properties file at the specified path. Verify that the path is correct and you have proper permissions to access it."}, }; } // @formatter:on From 151d40f28ed5865323fdfccf713f6f82aed6f913 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 13 Aug 2024 14:36:02 -0700 Subject: [PATCH 42/57] Added a proper file read --- mssql-jdbc.properties | 1 + .../configurableretry/ConfigurableRetryLogicTest.java | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 mssql-jdbc.properties diff --git a/mssql-jdbc.properties b/mssql-jdbc.properties new file mode 100644 index 000000000..608c46d04 --- /dev/null +++ b/mssql-jdbc.properties @@ -0,0 +1 @@ +retryExec={2716:1,2*2:CREATE;2714:1,2*2:CREATE}; \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index d0926d258..c04c675fd 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -8,6 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.io.File; +import java.io.FileWriter; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DriverManager; @@ -249,10 +251,19 @@ public void multipleRules() { */ @Test public void readFromFile() { + File myObj = null; try { + myObj = new File("mssql-jdbc.properties"); + FileWriter myWriter = new FileWriter(myObj); + myWriter.write("retryExec={2716:1,2*2:CREATE;2714:1,2*2:CREATE};"); + myWriter.close(); testStatementRetry(""); } catch (Exception e) { Assertions.fail(TestResource.getResource("R_unexpectedException")); + } finally { + if (myObj != null) { + myObj.delete(); + } } } From 7aebfb2d9dbd75201e9817ca9bcae0f0ac03cdb1 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 13 Aug 2024 14:36:35 -0700 Subject: [PATCH 43/57] Delete mssql-jdbc.properties --- mssql-jdbc.properties | 1 - 1 file changed, 1 deletion(-) delete mode 100644 mssql-jdbc.properties diff --git a/mssql-jdbc.properties b/mssql-jdbc.properties deleted file mode 100644 index 608c46d04..000000000 --- a/mssql-jdbc.properties +++ /dev/null @@ -1 +0,0 @@ -retryExec={2716:1,2*2:CREATE;2714:1,2*2:CREATE}; \ No newline at end of file From 763144db3ce7ad78f40566446f394e4f052c87e7 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Tue, 13 Aug 2024 19:26:42 -0700 Subject: [PATCH 44/57] Added more coverage and minor fixes, ready for review again --- .../sqlserver/jdbc/ConfigRetryRule.java | 41 ++++++++----------- .../jdbc/ConfigurableRetryLogic.java | 37 +++++++---------- .../sqlserver/jdbc/SQLServerStatement.java | 30 +++++--------- .../ConfigurableRetryLogicTest.java | 14 +++---- 4 files changed, 51 insertions(+), 71 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java index c2e407d4c..82f566a81 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java @@ -17,18 +17,13 @@ public class ConfigRetryRule { private String retryError; private String operand = "+"; - private final String PLUS_SIGN = "+"; - private final String MULTIPLICATION_SIGN = "*"; private int initialRetryTime = 0; private int retryChange = 2; private int retryCount = 1; private String retryQueries = ""; - private final String NON_POSITIVE_INT = "Not a positive number"; - private final String TOO_MANY_ARGS = "Too many arguments"; - private final String OPEN_BRACE = "{"; - private final String CLOSING_BRACE = "}"; + private final String PLUS_SIGN = "+"; + private final String MULTIPLICATION_SIGN = "*"; private final String COMMA = ","; - private final String COLON = ":"; private final String ZERO = "0"; private ArrayList waitTimes = new ArrayList<>(); @@ -43,7 +38,7 @@ public class ConfigRetryRule { */ public ConfigRetryRule(String rule) throws SQLServerException { addElements(parse(rule)); - calcWaitTime(); + calculateWaitTimes(); } /** @@ -72,15 +67,15 @@ private void copyFromExisting(ConfigRetryRule base) { } private String[] parse(String rule) { - if (rule.endsWith(COLON)) { + if (rule.endsWith(":")) { rule = rule + ZERO; // Add a zero to make below parsing easier } - rule = rule.replace(OPEN_BRACE, ""); - rule = rule.replace(CLOSING_BRACE, ""); + rule = rule.replace("{", ""); + rule = rule.replace("}", ""); rule = rule.trim(); - return rule.split(COLON); + return rule.split(":"); // Split on colon } /** @@ -92,13 +87,13 @@ private String[] parse(String rule) { * @throws SQLServerException * if a non-numeric value is passed in */ - private void parameterIsNumeric(String value) throws SQLServerException { + private void checkParameterIsNumeric(String value) throws SQLServerException { if (!StringUtils.isNumeric(value)) { String[] arr = value.split(COMMA); for (String error : arr) { if (!StringUtils.isNumeric(error)) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); - Object[] msgArgs = {error, NON_POSITIVE_INT}; + Object[] msgArgs = {error, "Not a positive number"}; throw new SQLServerException(null, form.format(msgArgs), null, 0, true); } } @@ -145,44 +140,44 @@ private void parameterIsNumeric(String value) throws SQLServerException { */ private void addElements(String[] rule) throws SQLServerException { if (rule.length == 2 || rule.length == 3) { - parameterIsNumeric(rule[0]); + checkParameterIsNumeric(rule[0]); retryError = rule[0]; String[] timings = rule[1].split(COMMA); - parameterIsNumeric(timings[0]); + checkParameterIsNumeric(timings[0]); retryCount = Integer.parseInt(timings[0]); if (timings.length == 2) { if (timings[1].contains(MULTIPLICATION_SIGN)) { String[] initialAndChange = timings[1].split("\\*"); - parameterIsNumeric(initialAndChange[0]); + checkParameterIsNumeric(initialAndChange[0]); initialRetryTime = Integer.parseInt(initialAndChange[0]); operand = MULTIPLICATION_SIGN; if (initialAndChange.length > 1) { - parameterIsNumeric(initialAndChange[1]); + checkParameterIsNumeric(initialAndChange[1]); retryChange = Integer.parseInt(initialAndChange[1]); } else { retryChange = initialRetryTime; } } else if (timings[1].contains(PLUS_SIGN)) { String[] initialAndChange = timings[1].split("\\+"); - parameterIsNumeric(initialAndChange[0]); + checkParameterIsNumeric(initialAndChange[0]); initialRetryTime = Integer.parseInt(initialAndChange[0]); operand = PLUS_SIGN; if (initialAndChange.length > 1) { - parameterIsNumeric(initialAndChange[1]); + checkParameterIsNumeric(initialAndChange[1]); retryChange = Integer.parseInt(initialAndChange[1]); } else { retryChange = 2; } } else { - parameterIsNumeric(timings[1]); + checkParameterIsNumeric(timings[1]); initialRetryTime = Integer.parseInt(timings[1]); } } else if (timings.length > 2) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); - Object[] msgArgs = {rule[1], TOO_MANY_ARGS}; + Object[] msgArgs = {rule[1], "Too many arguments"}; throw new SQLServerException(null, form.format(msgArgs), null, 0, true); } @@ -196,7 +191,7 @@ private void addElements(String[] rule) throws SQLServerException { } } - private void calcWaitTime() { + private void calculateWaitTimes() { for (int i = 0; i < retryCount; ++i) { int waitTime = initialRetryTime; if (operand.equals(PLUS_SIGN)) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 1bba660f9..f0689b613 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -88,33 +88,26 @@ private static void refreshRuleSet() throws SQLServerException { if ((currentTime - timeLastRead) >= INTERVAL_BETWEEN_READS_IN_MS) { // If it has been 30 secs, reread timeLastRead = currentTime; - if (timeLastModified != 0 && rulesHaveBeenChanged()) { - // If timeLastModified has been set, we have previously read from a file - setUpRules(null); + if (timeLastModified != 0) { + // If timeLastModified has been set, we have previously read from a file, so we setUpRules + // reading from file + File f = new File(getCurrentClassPath()); + if (f.lastModified() != timeLastModified) { + setUpRules(null); + } } else { setUpRules(prevRulesFromConnectionString); } } } - private static boolean rulesHaveBeenChanged() throws SQLServerException { - String inputToUse = getCurrentClassPath() + DEFAULT_PROPS_FILE; - - try { - File f = new File(inputToUse); - return f.lastModified() != timeLastModified; - } catch (Exception e) { - return true; - } - } - - void setFromConnectionString(String custom) throws SQLServerException { - prevRulesFromConnectionString = custom; + void setFromConnectionString(String newRules) throws SQLServerException { + prevRulesFromConnectionString = newRules; setUpRules(prevRulesFromConnectionString); } - void storeLastQuery(String sql) { - lastQuery = sql.toLowerCase(); + void storeLastQuery(String newQueryToStore) { + lastQuery = newQueryToStore.toLowerCase(); } String getLastQuery() { @@ -171,7 +164,7 @@ private static String getCurrentClassPath() throws SQLServerException { location = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath(); location = location.substring(0, location.length() - 16); URI uri = new URI(location + FORWARD_SLASH); - return uri.getPath(); + return uri.getPath() + DEFAULT_PROPS_FILE; // For now, we only allow "mssql-jdbc.properties" as file name. } catch (URISyntaxException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVURLInvalid")); Object[] msgArgs = {location + FORWARD_SLASH}; @@ -188,7 +181,7 @@ private static LinkedList readFromFile() throws SQLServerException { LinkedList list = new LinkedList<>(); try { - File f = new File(filePath + DEFAULT_PROPS_FILE); + File f = new File(filePath); try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { String readLine; while ((readLine = buffer.readLine()) != null) { @@ -213,10 +206,10 @@ private static LinkedList readFromFile() throws SQLServerException { return list; } - ConfigRetryRule searchRuleSet(int ruleToSearch) throws SQLServerException { + ConfigRetryRule searchRuleSet(int ruleToSearchFor) throws SQLServerException { refreshRuleSet(); for (Map.Entry entry : stmtRules.entrySet()) { - if (entry.getKey() == ruleToSearch) { + if (entry.getKey() == ruleToSearchFor) { return entry.getValue(); } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index e0aaa0c39..9a651f22d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -289,26 +289,18 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ } if (matchesDefinedQuery) { - try { - int timeToWait = rule.getWaitTimes().get(retryAttempt); - int queryTimeout = connection.getQueryTimeoutSeconds(); - if (queryTimeout >= 0 && timeToWait > queryTimeout) { - MessageFormat form = new MessageFormat( - SQLServerException.getErrString("R_InvalidRetryInterval")); - Object[] msgArgs = {timeToWait, queryTimeout}; - throw new SQLServerException(null, form.format(msgArgs), null, 0, true); - } - try { - Thread.sleep(TimeUnit.SECONDS.toMillis(timeToWait)); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } catch (IndexOutOfBoundsException exc) { + int timeToWait = rule.getWaitTimes().get(retryAttempt); + int queryTimeout = connection.getQueryTimeoutSeconds(); + if (queryTimeout >= 0 && timeToWait > queryTimeout) { MessageFormat form = new MessageFormat( - SQLServerException.getErrString("R_indexOutOfRange")); - Object[] msgArgs = {retryAttempt}; - throw new SQLServerException(this, form.format(msgArgs), null, 0, false); - + SQLServerException.getErrString("R_InvalidRetryInterval")); + Object[] msgArgs = {timeToWait, queryTimeout}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(timeToWait)); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); } cont = true; retryAttempt++; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index c04c675fd..d0140f347 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -251,18 +251,18 @@ public void multipleRules() { */ @Test public void readFromFile() { - File myObj = null; + File propsFile = null; try { - myObj = new File("mssql-jdbc.properties"); - FileWriter myWriter = new FileWriter(myObj); - myWriter.write("retryExec={2716:1,2*2:CREATE;2714:1,2*2:CREATE};"); - myWriter.close(); + propsFile = new File("mssql-jdbc.properties"); + FileWriter propFileWriter = new FileWriter(propsFile); + propFileWriter.write("retryExec={2716:1,2*2:CREATE;2714:1,2*2:CREATE};"); testStatementRetry(""); + propFileWriter.close(); } catch (Exception e) { Assertions.fail(TestResource.getResource("R_unexpectedException")); } finally { - if (myObj != null) { - myObj.delete(); + if (propsFile != null && !propsFile.delete()) { // If unable to delete, fail test + Assertions.fail(TestResource.getResource("R_unexpectedException")); } } } From 7e267b1b30b801142d8751190d00213dfbc7bf47 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Wed, 14 Aug 2024 08:59:47 -0700 Subject: [PATCH 45/57] Fixed read file test --- .../jdbc/configurableretry/ConfigurableRetryLogicTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index d0140f347..2fa4d7dd2 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -256,8 +256,9 @@ public void readFromFile() { propsFile = new File("mssql-jdbc.properties"); FileWriter propFileWriter = new FileWriter(propsFile); propFileWriter.write("retryExec={2716:1,2*2:CREATE;2714:1,2*2:CREATE};"); - testStatementRetry(""); propFileWriter.close(); + testStatementRetry(""); + } catch (Exception e) { Assertions.fail(TestResource.getResource("R_unexpectedException")); } finally { From a50fdf9f74773d88bf57151d0a3fac02c46aa97d Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Thu, 15 Aug 2024 09:33:26 -0700 Subject: [PATCH 46/57] Addressed recent pr comments --- .../jdbc/ConfigurableRetryLogic.java | 21 ++-- ...ryRule.java => ConfigurableRetryRule.java} | 100 +++++++----------- .../sqlserver/jdbc/SQLServerConnection.java | 12 +++ .../sqlserver/jdbc/SQLServerStatement.java | 8 +- .../ConfigurableRetryLogicTest.java | 22 ++-- 5 files changed, 78 insertions(+), 85 deletions(-) rename src/main/java/com/microsoft/sqlserver/jdbc/{ConfigRetryRule.java => ConfigurableRetryRule.java} (78%) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index f0689b613..8a12e3f1c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -32,12 +32,12 @@ public class ConfigurableRetryLogic { private final static String DEFAULT_PROPS_FILE = "mssql-jdbc.properties"; private static final java.util.logging.Logger CONFIGURABLE_RETRY_LOGGER = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); - private static ConfigurableRetryLogic driverInstance = null; + private static ConfigurableRetryLogic singleInstance = null; private static long timeLastModified; private static long timeLastRead; private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) private static String prevRulesFromConnectionString = ""; - private static HashMap stmtRules = new HashMap<>(); + private static HashMap stmtRules = new HashMap<>(); private static final Lock CRL_LOCK = new ReentrantLock(); private static final String SEMI_COLON = ";"; private static final String COMMA = ","; @@ -59,11 +59,11 @@ private ConfigurableRetryLogic() throws SQLServerException { * an exception */ public static ConfigurableRetryLogic getInstance() throws SQLServerException { - if (driverInstance == null) { + if (singleInstance == null) { CRL_LOCK.lock(); try { - if (driverInstance == null) { - driverInstance = new ConfigurableRetryLogic(); + if (singleInstance == null) { + singleInstance = new ConfigurableRetryLogic(); } else { refreshRuleSet(); } @@ -73,7 +73,7 @@ public static ConfigurableRetryLogic getInstance() throws SQLServerException { } else { refreshRuleSet(); } - return driverInstance; + return singleInstance; } /** @@ -86,7 +86,6 @@ public static ConfigurableRetryLogic getInstance() throws SQLServerException { private static void refreshRuleSet() throws SQLServerException { long currentTime = new Date().getTime(); if ((currentTime - timeLastRead) >= INTERVAL_BETWEEN_READS_IN_MS) { - // If it has been 30 secs, reread timeLastRead = currentTime; if (timeLastModified != 0) { // If timeLastModified has been set, we have previously read from a file, so we setUpRules @@ -140,13 +139,13 @@ private static void createRules(LinkedList listOfRules) throws SQLServer stmtRules = new HashMap<>(); for (String potentialRule : listOfRules) { - ConfigRetryRule rule = new ConfigRetryRule(potentialRule); + ConfigurableRetryRule rule = new ConfigurableRetryRule(potentialRule); if (rule.getError().contains(COMMA)) { String[] arr = rule.getError().split(COMMA); for (String retryError : arr) { - ConfigRetryRule splitRule = new ConfigRetryRule(retryError, rule); + ConfigurableRetryRule splitRule = new ConfigurableRetryRule(retryError, rule); stmtRules.put(Integer.parseInt(splitRule.getError()), splitRule); } } else { @@ -206,9 +205,9 @@ private static LinkedList readFromFile() throws SQLServerException { return list; } - ConfigRetryRule searchRuleSet(int ruleToSearchFor) throws SQLServerException { + ConfigurableRetryRule searchRuleSet(int ruleToSearchFor) throws SQLServerException { refreshRuleSet(); - for (Map.Entry entry : stmtRules.entrySet()) { + for (Map.Entry entry : stmtRules.entrySet()) { if (entry.getKey() == ruleToSearchFor) { return entry.getValue(); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java similarity index 78% rename from src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java rename to src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java index 82f566a81..5fc4c06f3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigRetryRule.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java @@ -14,7 +14,7 @@ * ConfigRetryRule object allows for one rule. * */ -public class ConfigRetryRule { +class ConfigurableRetryRule { private String retryError; private String operand = "+"; private int initialRetryTime = 0; @@ -29,15 +29,15 @@ public class ConfigRetryRule { private ArrayList waitTimes = new ArrayList<>(); /** - * Default constructor + * Construct a ConfigurableRetryRule object from a String rule. * * @param rule * The rule used to construct the ConfigRetryRule object * @throws SQLServerException * If there is a problem parsing the rule */ - public ConfigRetryRule(String rule) throws SQLServerException { - addElements(parse(rule)); + public ConfigurableRetryRule(String rule) throws SQLServerException { + addElements(removeExtraElementsAndSplitRule(rule)); calculateWaitTimes(); } @@ -46,27 +46,33 @@ public ConfigRetryRule(String rule) throws SQLServerException { * multiple errors provided. We pass in the multi-error object and create 1 new object for each error in the initial * object. * - * @param rule + * @param newRule * The rule used to construct the ConfigRetryRule object - * @param base + * @param baseRule * The ConfigRetryRule object to base the new objects off of */ - public ConfigRetryRule(String rule, ConfigRetryRule base) { - copyFromExisting(base); - this.retryError = rule; + public ConfigurableRetryRule(String newRule, ConfigurableRetryRule baseRule) { + copyFromRule(baseRule); + this.retryError = newRule; } - private void copyFromExisting(ConfigRetryRule base) { - this.retryError = base.getError(); - this.operand = base.getOperand(); - this.initialRetryTime = base.getInitialRetryTime(); - this.retryChange = base.getRetryChange(); - this.retryCount = base.getRetryCount(); - this.retryQueries = base.getRetryQueries(); - this.waitTimes = base.getWaitTimes(); + private void copyFromRule(ConfigurableRetryRule baseRule) { + this.retryError = baseRule.retryError; + this.operand = baseRule.operand; + this.initialRetryTime = baseRule.initialRetryTime; + this.retryChange = baseRule.retryChange; + this.retryCount = baseRule.retryCount; + this.retryQueries = baseRule.retryQueries; + this.waitTimes = baseRule.waitTimes; } - private String[] parse(String rule) { + /** + * Removes extra elements in the rule (e.g. '{') and splits the rule based on ':' (colon). + * + * @param rule + * @return + */ + private String[] removeExtraElementsAndSplitRule(String rule) { if (rule.endsWith(":")) { rule = rule + ZERO; // Add a zero to make below parsing easier } @@ -87,7 +93,7 @@ private String[] parse(String rule) { * @throws SQLServerException * if a non-numeric value is passed in */ - private void checkParameterIsNumeric(String value) throws SQLServerException { + private void checkParameter(String value) throws SQLServerException { if (!StringUtils.isNumeric(value)) { String[] arr = value.split(COMMA); for (String error : arr) { @@ -140,39 +146,39 @@ private void checkParameterIsNumeric(String value) throws SQLServerException { */ private void addElements(String[] rule) throws SQLServerException { if (rule.length == 2 || rule.length == 3) { - checkParameterIsNumeric(rule[0]); + checkParameter(rule[0]); retryError = rule[0]; String[] timings = rule[1].split(COMMA); - checkParameterIsNumeric(timings[0]); + checkParameter(timings[0]); retryCount = Integer.parseInt(timings[0]); if (timings.length == 2) { if (timings[1].contains(MULTIPLICATION_SIGN)) { String[] initialAndChange = timings[1].split("\\*"); - checkParameterIsNumeric(initialAndChange[0]); + checkParameter(initialAndChange[0]); initialRetryTime = Integer.parseInt(initialAndChange[0]); operand = MULTIPLICATION_SIGN; if (initialAndChange.length > 1) { - checkParameterIsNumeric(initialAndChange[1]); + checkParameter(initialAndChange[1]); retryChange = Integer.parseInt(initialAndChange[1]); } else { retryChange = initialRetryTime; } } else if (timings[1].contains(PLUS_SIGN)) { String[] initialAndChange = timings[1].split("\\+"); - checkParameterIsNumeric(initialAndChange[0]); + checkParameter(initialAndChange[0]); initialRetryTime = Integer.parseInt(initialAndChange[0]); operand = PLUS_SIGN; if (initialAndChange.length > 1) { - checkParameterIsNumeric(initialAndChange[1]); + checkParameter(initialAndChange[1]); retryChange = Integer.parseInt(initialAndChange[1]); } else { retryChange = 2; } } else { - checkParameterIsNumeric(timings[1]); + checkParameter(timings[1]); initialRetryTime = Integer.parseInt(timings[1]); } } else if (timings.length > 2) { @@ -191,6 +197,10 @@ private void addElements(String[] rule) throws SQLServerException { } } + /** + * Calculates all the 'wait times', i.e. how long the driver waits between re-execution of statement, based + * on the parameters in the rule. Saves all these times in "waitTimes" to be referenced during statement re-execution. + */ private void calculateWaitTimes() { for (int i = 0; i < retryCount; ++i) { int waitTime = initialRetryTime; @@ -213,47 +223,17 @@ private void calculateWaitTimes() { * @return * The retry error */ - public String getError() { + String getError() { return retryError; } - /** - * Returns the retry error for this ConfigRetryRule object. - * - * @return - * The retry error - */ - public String getOperand() { - return operand; - } - - /** - * Returns the retry error (errors to retry on) for this ConfigRetryRule object. - * - * @return - * The retry error - */ - public int getInitialRetryTime() { - return initialRetryTime; - } - - /** - * Returns the retry change (timing change to apply to wait times) for this ConfigRetryRule object. - * - * @return - * The retry change - */ - public int getRetryChange() { - return retryChange; - } - /** * Returns the retry count (amount of times to retry) for this ConfigRetryRule object. * * @return * The retry count */ - public int getRetryCount() { + int getRetryCount() { return retryCount; } @@ -263,7 +243,7 @@ public int getRetryCount() { * @return * The retry query specifier */ - public String getRetryQueries() { + String getRetryQueries() { return retryQueries; } @@ -273,7 +253,7 @@ public String getRetryQueries() { * @return * The list of waiting times */ - public ArrayList getWaitTimes() { + ArrayList getWaitTimes() { return waitTimes; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 1b43fb84f..4446e14e9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1074,10 +1074,22 @@ public void setCalcBigDecimalPrecision(boolean calcBigDecimalPrecision) { private String retryExec = SQLServerDriverStringProperty.RETRY_EXEC.getDefaultValue(); + /** + * Returns the set of configurable statement retry rules set in retryExec + * + * @return + * A string containing statement retry rules. + */ public String getRetryExec() { return retryExec; } + /** + * Sets the list of configurable statement retry rules, for the given connection, in retryExec. + * + * @param retryExec + * The list of retry rules to set, as a string. + */ public void setRetryExec(String retryExec) { this.retryExec = retryExec; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 9a651f22d..c47e775a4 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -261,6 +261,7 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ boolean cont; int retryAttempt = 0; + ConfigurableRetryLogic crl = ConfigurableRetryLogic.getInstance(); do { cont = false; @@ -269,10 +270,10 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ executeCommand(newStmtCmd); } catch (SQLServerException e) { SQLServerError sqlServerError = e.getSQLServerError(); - ConfigRetryRule rule = null; + ConfigurableRetryRule rule = null; if (null != sqlServerError) { - rule = ConfigurableRetryLogic.getInstance().searchRuleSet(e.getSQLServerError().getErrorNumber()); + rule = crl.searchRuleSet(e.getSQLServerError().getErrorNumber()); } // If there is a rule for this error AND we still have retries remaining THEN we can proceed, otherwise @@ -284,8 +285,7 @@ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQ boolean matchesDefinedQuery = true; if (!(rule.getRetryQueries().isEmpty())) { - matchesDefinedQuery = rule.getRetryQueries() - .contains(ConfigurableRetryLogic.getInstance().getLastQuery().split(" ")[0]); + matchesDefinedQuery = rule.getRetryQueries().contains(crl.getLastQuery().split(" ")[0]); } if (matchesDefinedQuery) { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index 2fa4d7dd2..8447af2ee 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -45,7 +45,8 @@ public static void setupTests() throws Exception { } /** - * Test that the SQLServerConnection methods getRetryExec and setRetryExec are working. + * Test that the SQLServerConnection methods getRetryExec and setRetryExec correctly get the existing retryExec, and + * set the retryExec connection parameter respectively. * * @throws Exception * if an exception occurs @@ -57,8 +58,8 @@ public void testRetryExecConnectionStringOption() throws Exception { String test = conn.getRetryExec(); assertTrue(test.isEmpty()); conn.setRetryExec("{2714:3,2*2:CREATE;2715:1,3}"); - PreparedStatement ps = conn.prepareStatement("create table " + tableName + " (c1 int null);"); try { + PreparedStatement ps = conn.prepareStatement("create table " + tableName + " (c1 int null);"); createTable(s); ps.execute(); Assertions.fail(TestResource.getResource("R_expectedFailPassed")); @@ -72,7 +73,7 @@ public void testRetryExecConnectionStringOption() throws Exception { } /** - * Tests that statement retry works with prepared statements. + * Tests that statement retry with prepared statements correctly retries given the provided retryExec rule. * * @throws Exception * if unable to connect or execute against db @@ -82,8 +83,8 @@ public void testStatementRetryPreparedStatement() throws Exception { connectionStringCRL = TestUtils.addOrOverrideProperty(connectionString, "retryExec", "{2714:3,2*2:CREATE;2715:1,3}"); - try (Connection conn = DriverManager.getConnection(connectionStringCRL); Statement s = conn.createStatement()) { - PreparedStatement ps = conn.prepareStatement("create table " + tableName + " (c1 int null);"); + try (Connection conn = DriverManager.getConnection(connectionStringCRL); Statement s = conn.createStatement(); + PreparedStatement ps = conn.prepareStatement("create table " + tableName + " (c1 int null);")) { try { createTable(s); ps.execute(); @@ -98,7 +99,7 @@ public void testStatementRetryPreparedStatement() throws Exception { } /** - * Tests that statement retry works with callable statements. + * Tests that statement retry with callable statements correctly retries given the provided retryExec rule. * * @throws Exception * if unable to connect or execute against db @@ -125,7 +126,7 @@ public void testStatementRetryCallableStatement() throws Exception { } /** - * Tests that statement retry works with statements. Used in below negative testing. + * Tests that statement retry with SQL server statements correctly retries given the provided retryExec rule. * * @throws Exception * if unable to connect or execute against db @@ -148,8 +149,9 @@ public void testStatementRetry(String addedRetryParams) throws Exception { } /** - * Tests that statement retry works with statements. A different error is expected here than the test above. - * + * Tests that statement retry with SQL server statements correctly attempts to retry, but eventually cancels due + * to the retry wait interval being longer than queryTimeout. + * * @throws Exception * if unable to connect or execute against db */ @@ -235,7 +237,7 @@ public void statementTimingTests() { } /** - * Tests that CRL works with multiple rules provided at once. + * Tests that configurable retry logic correctly parses, and retries using, multiple rules provided at once. */ @Test public void multipleRules() { From 04c25f90d7c3ed6ce7069d8829a47481297c3005 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 23 Aug 2024 12:55:41 -0700 Subject: [PATCH 47/57] Remove double locking --- .../sqlserver/jdbc/ConfigurableRetryLogic.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 8a12e3f1c..18c70c5d7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -60,16 +60,7 @@ private ConfigurableRetryLogic() throws SQLServerException { */ public static ConfigurableRetryLogic getInstance() throws SQLServerException { if (singleInstance == null) { - CRL_LOCK.lock(); - try { - if (singleInstance == null) { - singleInstance = new ConfigurableRetryLogic(); - } else { - refreshRuleSet(); - } - } finally { - CRL_LOCK.unlock(); - } + singleInstance = new ConfigurableRetryLogic(); } else { refreshRuleSet(); } From c0fa29a02083b78b3424ffc3a05023b24a336de3 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 23 Aug 2024 12:57:50 -0700 Subject: [PATCH 48/57] Remove unneeded variable --- .../com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 18c70c5d7..dfbe4e9c4 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -18,8 +18,6 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.Map; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; /** @@ -38,7 +36,6 @@ public class ConfigurableRetryLogic { private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) private static String prevRulesFromConnectionString = ""; private static HashMap stmtRules = new HashMap<>(); - private static final Lock CRL_LOCK = new ReentrantLock(); private static final String SEMI_COLON = ";"; private static final String COMMA = ","; private static final String FORWARD_SLASH = "/"; @@ -59,6 +56,7 @@ private ConfigurableRetryLogic() throws SQLServerException { * an exception */ public static ConfigurableRetryLogic getInstance() throws SQLServerException { + // No need for lock; static initializer singleInstance is thread-safe if (singleInstance == null) { singleInstance = new ConfigurableRetryLogic(); } else { From cc1540c643e56302b2b8aeda9db3bbca4e7d64b8 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Wed, 18 Sep 2024 14:19:02 -0700 Subject: [PATCH 49/57] Revisions after PR review --- .../sqlserver/jdbc/ConfigurableRetryLogic.java | 2 +- .../sqlserver/jdbc/ConfigurableRetryRule.java | 12 ++++++------ .../sqlserver/jdbc/SQLServerResource.java | 1 - .../ConfigurableRetryLogicTest.java | 14 +++++++------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index dfbe4e9c4..b3f9856a7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -184,7 +184,7 @@ private static LinkedList readFromFile() throws SQLServerException { // If the file is not found either A) We're not using CRL OR B) the path is wrong. Do not error out, instead // log a message. if (CONFIGURABLE_RETRY_LOGGER.isLoggable(java.util.logging.Level.FINER)) { - CONFIGURABLE_RETRY_LOGGER.finest("No properties file exists or the file path is incorrect."); + CONFIGURABLE_RETRY_LOGGER.finest("File not found at path - " + filePath); } } catch (IOException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream")); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java index 5fc4c06f3..248d2b993 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java @@ -36,7 +36,7 @@ class ConfigurableRetryRule { * @throws SQLServerException * If there is a problem parsing the rule */ - public ConfigurableRetryRule(String rule) throws SQLServerException { + ConfigurableRetryRule(String rule) throws SQLServerException { addElements(removeExtraElementsAndSplitRule(rule)); calculateWaitTimes(); } @@ -51,7 +51,7 @@ public ConfigurableRetryRule(String rule) throws SQLServerException { * @param baseRule * The ConfigRetryRule object to base the new objects off of */ - public ConfigurableRetryRule(String newRule, ConfigurableRetryRule baseRule) { + ConfigurableRetryRule(String newRule, ConfigurableRetryRule baseRule) { copyFromRule(baseRule); this.retryError = newRule; } @@ -98,8 +98,8 @@ private void checkParameter(String value) throws SQLServerException { String[] arr = value.split(COMMA); for (String error : arr) { if (!StringUtils.isNumeric(error)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); - Object[] msgArgs = {error, "Not a positive number"}; + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidParameterNumber")); + Object[] msgArgs = {error}; throw new SQLServerException(null, form.format(msgArgs), null, 0, true); } } @@ -182,8 +182,8 @@ private void addElements(String[] rule) throws SQLServerException { initialRetryTime = Integer.parseInt(timings[1]); } } else if (timings.length > 2) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidParameterFormat")); - Object[] msgArgs = {rule[1], "Too many arguments"}; + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidParameterNumber")); + Object[] msgArgs = {rule[1]}; throw new SQLServerException(null, form.format(msgArgs), null, 0, true); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 99165f140..539b9797f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -546,7 +546,6 @@ protected Object[][] getContents() { {"R_InvalidSqlQuery", "Invalid SQL Query: {0}"}, {"R_InvalidScale", "Scale of input value is larger than the maximum allowed by SQL Server."}, {"R_colCountNotMatchColTypeCount", "Number of provided columns {0} does not match the column data types definition {1}."}, - {"R_InvalidParameterFormat", "One or more supplied parameters is/are not correct: {0}, for the reason: {1}."}, {"R_InvalidRuleFormat", "Wrong number of parameters supplied to rule. Number of parameters: {0}, expected: 2 or 3."}, {"R_InvalidRetryInterval", "Current retry interval: {0}, is longer than queryTimeout: {1}."}, {"R_UnableToFindClass", "Unable to locate specified class: {0}"}, diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index 8447af2ee..2e0d2d2b6 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -337,7 +337,7 @@ public void testTooManyTimings() { try { testStatementRetry("retryExec={2714,2716:1,2*2,1:CREATE};"); } catch (SQLServerException e) { - assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_invalidParameterNumber"))); } catch (Exception e) { Assertions.fail(TestResource.getResource("R_unexpectedException")); } @@ -355,14 +355,14 @@ public void testRetryError() throws Exception { try { testStatementRetry("retryExec={TEST:TEST};"); } catch (SQLServerException e) { - assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_invalidParameterNumber"))); } // Test empty error try { testStatementRetry("retryExec={:1,2*2:CREATE};"); } catch (SQLServerException e) { - assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_invalidParameterNumber"))); } } @@ -378,7 +378,7 @@ public void testRetryCount() throws Exception { try { testStatementRetry("retryExec={2714,2716:-1,2+2:CREATE};"); } catch (SQLServerException e) { - assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_invalidParameterNumber"))); } // Test max (query timeout) @@ -401,7 +401,7 @@ public void testInitialRetryTime() throws Exception { try { testStatementRetry("retryExec={2714,2716:4,-1+1:CREATE};"); } catch (SQLServerException e) { - assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_invalidParameterNumber"))); } // Test max @@ -424,7 +424,7 @@ public void testOperand() throws Exception { try { testStatementRetry("retryExec={2714,2716:1,2AND2:CREATE};"); } catch (SQLServerException e) { - assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_invalidParameterNumber"))); } } @@ -440,7 +440,7 @@ public void testRetryChange() throws Exception { try { testStatementRetry("retryExec={2714,2716:1,2+2:CREATE};"); } catch (SQLServerException e) { - assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidParameterFormat"))); + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_invalidParameterNumber"))); } } From 9269c18d6d953f583f2b2ded9c33a015b21aa993 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Wed, 18 Sep 2024 19:46:12 -0700 Subject: [PATCH 50/57] PR review update --- .../com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index b3f9856a7..b8ea8a4ed 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -184,11 +184,11 @@ private static LinkedList readFromFile() throws SQLServerException { // If the file is not found either A) We're not using CRL OR B) the path is wrong. Do not error out, instead // log a message. if (CONFIGURABLE_RETRY_LOGGER.isLoggable(java.util.logging.Level.FINER)) { - CONFIGURABLE_RETRY_LOGGER.finest("File not found at path - " + filePath); + CONFIGURABLE_RETRY_LOGGER.finest("File not found at path - \"" + filePath + "\""); } } catch (IOException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream")); - Object[] msgArgs = {e.toString()}; + Object[] msgArgs = {e.getMessage() + ", from path - \"" + filePath + "\""}; throw new SQLServerException(form.format(msgArgs), null, 0, e); } return list; From bbcdf8db044db3a80b65019abb09153dc8746743 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Thu, 19 Sep 2024 16:59:32 -0700 Subject: [PATCH 51/57] Rename R_AKVURLInvalid as its use is no longer AKV specific --- .../com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java | 2 +- .../jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java | 2 +- .../java/com/microsoft/sqlserver/jdbc/SQLServerResource.java | 2 +- .../jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index b8ea8a4ed..035b37d69 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -154,7 +154,7 @@ private static String getCurrentClassPath() throws SQLServerException { URI uri = new URI(location + FORWARD_SLASH); return uri.getPath() + DEFAULT_PROPS_FILE; // For now, we only allow "mssql-jdbc.properties" as file name. } catch (URISyntaxException e) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVURLInvalid")); + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_URLInvalid")); Object[] msgArgs = {location + FORWARD_SLASH}; throw new SQLServerException(form.format(msgArgs), null, 0, e); } catch (ClassNotFoundException e) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java index 42281688a..082028f2c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java @@ -645,7 +645,7 @@ private void validateNonEmptyAKVPath(String masterKeyPath) throws SQLServerExcep } } } catch (URISyntaxException e) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVURLInvalid")); + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_URLInvalid")); Object[] msgArgs = {masterKeyPath}; throw new SQLServerException(form.format(msgArgs), null, 0, e); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 539b9797f..55a5a5fc0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -370,8 +370,8 @@ protected Object[][] getContents() { {"R_ForceEncryptionTrue_HonorAEFalseRS", "Cannot set Force Encryption to true for parameter {0} because encryption is not enabled for the statement or procedure."}, {"R_ForceEncryptionTrue_HonorAETrue_UnencryptedColumnRS", "Cannot execute update because Force Encryption was set as true for parameter {0} and the database expects this parameter to be sent as plaintext. This may be due to a configuration error."}, {"R_NullValue", "{0} cannot be null."}, + {"R_URLInvalid", "Invalid URL specified: {0}."}, {"R_AKVPathNull", "Azure Key Vault key path cannot be null."}, - {"R_AKVURLInvalid", "Invalid URL specified: {0}."}, {"R_AKVMasterKeyPathInvalid", "Invalid Azure Key Vault key path specified: {0}."}, {"R_ManagedIdentityInitFail", "Failed to initialize package to get Managed Identity token for Azure Key Vault."}, {"R_EmptyCEK", "Empty column encryption key specified."}, diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java index 7376baca7..6f3502933 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java @@ -313,7 +313,7 @@ public void testAkvDecryptColumnEncryptionKey(String serverName, String url, Str akv.decryptColumnEncryptionKey("http:///^[!#$&-;=?-[]_a-", "", null); fail(TestResource.getResource("R_expectedExceptionNotThrown")); } catch (SQLServerException e) { - assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_AKVURLInvalid")), e.getMessage()); + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_URLInvalid")), e.getMessage()); } // null encryptedColumnEncryptionKey From 091ed78ccd4ff4e353fcc2a7460cbec272a7ab08 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 20 Sep 2024 07:29:50 -0700 Subject: [PATCH 52/57] Add back logging --- .../jdbc/ConfigurableRetryLogic.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 035b37d69..a460c32d3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -18,6 +18,8 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; /** @@ -28,9 +30,10 @@ public class ConfigurableRetryLogic { private final static int INTERVAL_BETWEEN_READS_IN_MS = 30000; private final static String DEFAULT_PROPS_FILE = "mssql-jdbc.properties"; + private static final Lock CRL_LOCK = new ReentrantLock(); private static final java.util.logging.Logger CONFIGURABLE_RETRY_LOGGER = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); - private static ConfigurableRetryLogic singleInstance = null; + private static ConfigurableRetryLogic singleInstance; private static long timeLastModified; private static long timeLastRead; private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) @@ -56,12 +59,22 @@ private ConfigurableRetryLogic() throws SQLServerException { * an exception */ public static ConfigurableRetryLogic getInstance() throws SQLServerException { - // No need for lock; static initializer singleInstance is thread-safe + // No need for lock; static initializer singleInstance is thread-safe] if (singleInstance == null) { - singleInstance = new ConfigurableRetryLogic(); + CRL_LOCK.lock(); + try { + if (singleInstance == null) { + singleInstance = new ConfigurableRetryLogic(); + } else { + refreshRuleSet(); + } + } finally { + CRL_LOCK.unlock(); + } } else { refreshRuleSet(); } + return singleInstance; } From 9aa47ca30ab315f054ba02716e57432ea24bc101 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 20 Sep 2024 07:30:21 -0700 Subject: [PATCH 53/57] Typo --- .../com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index a460c32d3..3f59c3b5e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -59,7 +59,7 @@ private ConfigurableRetryLogic() throws SQLServerException { * an exception */ public static ConfigurableRetryLogic getInstance() throws SQLServerException { - // No need for lock; static initializer singleInstance is thread-safe] + // No need for lock; static initializer singleInstance is thread-safe if (singleInstance == null) { CRL_LOCK.lock(); try { From 49b7b44320320abdf946951ac2c2c1eb9b47ef41 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 20 Sep 2024 08:11:29 -0700 Subject: [PATCH 54/57] Removed unneeded comment --- .../com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 3f59c3b5e..4b46271b0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -59,7 +59,6 @@ private ConfigurableRetryLogic() throws SQLServerException { * an exception */ public static ConfigurableRetryLogic getInstance() throws SQLServerException { - // No need for lock; static initializer singleInstance is thread-safe if (singleInstance == null) { CRL_LOCK.lock(); try { From 34cfa4b52ee5fcc5821b99b0982862267536ba03 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 20 Sep 2024 09:53:48 -0700 Subject: [PATCH 55/57] Make static variables thread-safe --- .../jdbc/ConfigurableRetryLogic.java | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index 4b46271b0..eaee685d3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -18,6 +18,8 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -28,25 +30,28 @@ * */ public class ConfigurableRetryLogic { - private final static int INTERVAL_BETWEEN_READS_IN_MS = 30000; - private final static String DEFAULT_PROPS_FILE = "mssql-jdbc.properties"; + private static final int INTERVAL_BETWEEN_READS_IN_MS = 30000; + private static final String DEFAULT_PROPS_FILE = "mssql-jdbc.properties"; private static final Lock CRL_LOCK = new ReentrantLock(); private static final java.util.logging.Logger CONFIGURABLE_RETRY_LOGGER = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.ConfigurableRetryLogic"); - private static ConfigurableRetryLogic singleInstance; - private static long timeLastModified; - private static long timeLastRead; - private static String lastQuery = ""; // The last query executed (used when rule is process-dependent) - private static String prevRulesFromConnectionString = ""; - private static HashMap stmtRules = new HashMap<>(); private static final String SEMI_COLON = ";"; private static final String COMMA = ","; private static final String FORWARD_SLASH = "/"; private static final String EQUALS_SIGN = "="; private static final String RETRY_EXEC = "retryExec"; + private static final AtomicLong timeLastModified = new AtomicLong(0); + private static final AtomicLong timeLastRead = new AtomicLong(0); + private static final AtomicReference lastQuery + = new AtomicReference<>(""); // The last query executed (used when rule is process-dependent) + private static final AtomicReference prevRulesFromConnectionString = new AtomicReference<>(""); + private static final AtomicReference> stmtRules + = new AtomicReference<>(new HashMap<>()); + private static ConfigurableRetryLogic singleInstance; + private ConfigurableRetryLogic() throws SQLServerException { - timeLastRead = new Date().getTime(); + timeLastRead.compareAndSet(0, new Date().getTime()); setUpRules(null); } @@ -86,32 +91,32 @@ public static ConfigurableRetryLogic getInstance() throws SQLServerException { */ private static void refreshRuleSet() throws SQLServerException { long currentTime = new Date().getTime(); - if ((currentTime - timeLastRead) >= INTERVAL_BETWEEN_READS_IN_MS) { - timeLastRead = currentTime; - if (timeLastModified != 0) { + if ((currentTime - timeLastRead.get()) >= INTERVAL_BETWEEN_READS_IN_MS) { + timeLastRead.set(currentTime); + if (timeLastModified.get() != 0) { // If timeLastModified has been set, we have previously read from a file, so we setUpRules // reading from file File f = new File(getCurrentClassPath()); - if (f.lastModified() != timeLastModified) { + if (f.lastModified() != timeLastModified.get()) { setUpRules(null); } } else { - setUpRules(prevRulesFromConnectionString); + setUpRules(prevRulesFromConnectionString.get()); } } } void setFromConnectionString(String newRules) throws SQLServerException { - prevRulesFromConnectionString = newRules; - setUpRules(prevRulesFromConnectionString); + prevRulesFromConnectionString.set(newRules); + setUpRules(prevRulesFromConnectionString.get()); } void storeLastQuery(String newQueryToStore) { - lastQuery = newQueryToStore.toLowerCase(); + lastQuery.set(newQueryToStore.toLowerCase()); } String getLastQuery() { - return lastQuery; + return lastQuery.get(); } /** @@ -123,8 +128,8 @@ String getLastQuery() { * If an exception occurs */ private static void setUpRules(String cxnStrRules) throws SQLServerException { - stmtRules = new HashMap<>(); - lastQuery = ""; + stmtRules.set(new HashMap<>()); + lastQuery.set(""); LinkedList temp; if (cxnStrRules == null || cxnStrRules.isEmpty()) { @@ -137,7 +142,7 @@ private static void setUpRules(String cxnStrRules) throws SQLServerException { } private static void createRules(LinkedList listOfRules) throws SQLServerException { - stmtRules = new HashMap<>(); + stmtRules.set(new HashMap<>()); for (String potentialRule : listOfRules) { ConfigurableRetryRule rule = new ConfigurableRetryRule(potentialRule); @@ -147,10 +152,10 @@ private static void createRules(LinkedList listOfRules) throws SQLServer for (String retryError : arr) { ConfigurableRetryRule splitRule = new ConfigurableRetryRule(retryError, rule); - stmtRules.put(Integer.parseInt(splitRule.getError()), splitRule); + stmtRules.get().put(Integer.parseInt(splitRule.getError()), splitRule); } } else { - stmtRules.put(Integer.parseInt(rule.getError()), rule); + stmtRules.get().put(Integer.parseInt(rule.getError()), rule); } } } @@ -191,7 +196,7 @@ private static LinkedList readFromFile() throws SQLServerException { } } } - timeLastModified = f.lastModified(); + timeLastModified.set(f.lastModified()); } catch (FileNotFoundException e) { // If the file is not found either A) We're not using CRL OR B) the path is wrong. Do not error out, instead // log a message. @@ -208,7 +213,7 @@ private static LinkedList readFromFile() throws SQLServerException { ConfigurableRetryRule searchRuleSet(int ruleToSearchFor) throws SQLServerException { refreshRuleSet(); - for (Map.Entry entry : stmtRules.entrySet()) { + for (Map.Entry entry : stmtRules.get().entrySet()) { if (entry.getKey() == ruleToSearchFor) { return entry.getValue(); } From df67a98b7914329a21f4fba0ed52d1c4d9544dcd Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Fri, 20 Sep 2024 16:31:21 -0700 Subject: [PATCH 56/57] Timing --- .../jdbc/configurableretry/ConfigurableRetryLogicTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index 2e0d2d2b6..3e8431f76 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -214,7 +214,7 @@ public void statementTimingTests() { Assertions.fail(TestResource.getResource("R_unexpectedException")); } finally { totalTime = System.currentTimeMillis() - timerStart; - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(8), + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(2), "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(8)); assertTrue(totalTime < TimeUnit.SECONDS.toMillis(18), "total time: " + totalTime + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(18)); @@ -229,7 +229,7 @@ public void statementTimingTests() { Assertions.fail(TestResource.getResource("R_unexpectedException")); } finally { totalTime = System.currentTimeMillis() - timerStart; - assertTrue(totalTime > TimeUnit.SECONDS.toMillis(10), + assertTrue(totalTime > TimeUnit.SECONDS.toMillis(3), "total time: " + totalTime + ", expected minimum time: " + TimeUnit.SECONDS.toMillis(10)); assertTrue(totalTime < TimeUnit.SECONDS.toMillis(20), "total time: " + totalTime + ", expected maximum time: " + TimeUnit.SECONDS.toMillis(20)); @@ -383,7 +383,7 @@ public void testRetryCount() throws Exception { // Test max (query timeout) try { - testStatementRetryWithShortQueryTimeout("queryTimeout=10;retryExec={2714,2716:11,2+2:CREATE};"); + testStatementRetryWithShortQueryTimeout("queryTimeout=3;retryExec={2714,2716:11,2+2:CREATE};"); } catch (SQLServerException e) { assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidRetryInterval"))); } From 7c7fc92cff30c7c482d32ec7c5f01fcba11fe376 Mon Sep 17 00:00:00 2001 From: Jeff Wasty Date: Sun, 22 Sep 2024 00:59:40 -0700 Subject: [PATCH 57/57] JavaDoc cleanup. --- .../jdbc/ConfigurableRetryLogic.java | 97 ++++++++++++++++--- .../sqlserver/jdbc/ConfigurableRetryRule.java | 45 +++++---- .../ConfigurableRetryLogicTest.java | 73 ++++++++------ 3 files changed, 152 insertions(+), 63 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java index eaee685d3..ed5591034 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java @@ -27,7 +27,6 @@ /** * Allows configurable statement retry through the use of the 'retryExec' connection property. Each rule read in is * converted to ConfigRetryRule objects, which are stored and referenced during statement retry. - * */ public class ConfigurableRetryLogic { private static final int INTERVAL_BETWEEN_READS_IN_MS = 30000; @@ -40,26 +39,45 @@ public class ConfigurableRetryLogic { private static final String FORWARD_SLASH = "/"; private static final String EQUALS_SIGN = "="; private static final String RETRY_EXEC = "retryExec"; + /** + * The time the properties file was last modified. + */ private static final AtomicLong timeLastModified = new AtomicLong(0); + /** + * The time we last read the properties file. + */ private static final AtomicLong timeLastRead = new AtomicLong(0); - private static final AtomicReference lastQuery - = new AtomicReference<>(""); // The last query executed (used when rule is process-dependent) + /** + * The last query executed (used when rule is process-dependent). + */ + private static final AtomicReference lastQuery = new AtomicReference<>(""); + /** + * The previously read rules from the connection string. + */ private static final AtomicReference prevRulesFromConnectionString = new AtomicReference<>(""); - private static final AtomicReference> stmtRules - = new AtomicReference<>(new HashMap<>()); + /** + * The list of statement retry rules. + */ + private static final AtomicReference> stmtRules = new AtomicReference<>( + new HashMap<>()); private static ConfigurableRetryLogic singleInstance; - + /** + * Constructs the ConfigurableRetryLogic object reading rules from available sources. + * + * @throws SQLServerException + * if unable to construct + */ private ConfigurableRetryLogic() throws SQLServerException { timeLastRead.compareAndSet(0, new Date().getTime()); setUpRules(null); } /** - * Fetches the static instance of ConfigurableRetryLogic, instantiating it if it hasn't already been. - * Each time the instance is fetched, we check if a re-read is needed, and do so if properties should be re-read. + * Fetches the static instance of ConfigurableRetryLogic, instantiating it if it hasn't already been. Each time the + * instance is fetched, we check if a re-read is needed, and do so if properties should be re-read. * - * @return The static instance of ConfigurableRetryLogic + * @return the static instance of ConfigurableRetryLogic * @throws SQLServerException * an exception */ @@ -91,11 +109,11 @@ public static ConfigurableRetryLogic getInstance() throws SQLServerException { */ private static void refreshRuleSet() throws SQLServerException { long currentTime = new Date().getTime(); + if ((currentTime - timeLastRead.get()) >= INTERVAL_BETWEEN_READS_IN_MS) { timeLastRead.set(currentTime); if (timeLastModified.get() != 0) { - // If timeLastModified has been set, we have previously read from a file, so we setUpRules - // reading from file + // If timeLastModified is set, we previously read from file, so we setUpRules also reading from file File f = new File(getCurrentClassPath()); if (f.lastModified() != timeLastModified.get()) { setUpRules(null); @@ -106,15 +124,34 @@ private static void refreshRuleSet() throws SQLServerException { } } + /** + * Sets rules given from connection string. + * + * @param newRules + * the new rules to use + * @throws SQLServerException + * when an exception occurs + */ void setFromConnectionString(String newRules) throws SQLServerException { prevRulesFromConnectionString.set(newRules); setUpRules(prevRulesFromConnectionString.get()); } + /** + * Stores last query executed. + * + * @param newQueryToStore + * the new query to store + */ void storeLastQuery(String newQueryToStore) { lastQuery.set(newQueryToStore.toLowerCase()); } + /** + * Gets last query. + * + * @return the last query + */ String getLastQuery() { return lastQuery.get(); } @@ -123,14 +160,15 @@ String getLastQuery() { * Sets up rules based on either connection string option or file read. * * @param cxnStrRules - * If null, rules are constructed from file, else, this parameter is used to construct rules + * if null, rules are constructed from file, else, this parameter is used to construct rules * @throws SQLServerException - * If an exception occurs + * if an exception occurs */ private static void setUpRules(String cxnStrRules) throws SQLServerException { + LinkedList temp; + stmtRules.set(new HashMap<>()); lastQuery.set(""); - LinkedList temp; if (cxnStrRules == null || cxnStrRules.isEmpty()) { temp = readFromFile(); @@ -141,6 +179,14 @@ private static void setUpRules(String cxnStrRules) throws SQLServerException { createRules(temp); } + /** + * Creates and stores rules based on the inputted list of rules. + * + * @param listOfRules + * the list of rules, as a String LinkedList + * @throws SQLServerException + * if unable to create rules from the inputted list + */ private static void createRules(LinkedList listOfRules) throws SQLServerException { stmtRules.set(new HashMap<>()); @@ -160,6 +206,13 @@ private static void createRules(LinkedList listOfRules) throws SQLServer } } + /** + * Gets the current class path (for use in file reading). + * + * @return the current class path, as a String + * @throws SQLServerException + * if unable to retrieve the current class path + */ private static String getCurrentClassPath() throws SQLServerException { String location = ""; String className = ""; @@ -181,6 +234,13 @@ private static String getCurrentClassPath() throws SQLServerException { } } + /** + * Attempts to read rules from the properties file. + * + * @return the list of rules as a LinkedList + * @throws SQLServerException + * if unable to read from the file + */ private static LinkedList readFromFile() throws SQLServerException { String filePath = getCurrentClassPath(); LinkedList list = new LinkedList<>(); @@ -211,6 +271,15 @@ private static LinkedList readFromFile() throws SQLServerException { return list; } + /** + * Searches rule set for the given rule. + * + * @param ruleToSearchFor + * the rule to search for + * @return the configurable retry rule + * @throws SQLServerException + * when an exception occurs + */ ConfigurableRetryRule searchRuleSet(int ruleToSearchFor) throws SQLServerException { refreshRuleSet(); for (Map.Entry entry : stmtRules.get().entrySet()) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java index 248d2b993..f52df8d8d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryRule.java @@ -15,16 +15,16 @@ * */ class ConfigurableRetryRule { - private String retryError; + private final String PLUS_SIGN = "+"; + private final String MULTIPLICATION_SIGN = "*"; + private final String COMMA = ","; + private final String ZERO = "0"; private String operand = "+"; private int initialRetryTime = 0; private int retryChange = 2; private int retryCount = 1; private String retryQueries = ""; - private final String PLUS_SIGN = "+"; - private final String MULTIPLICATION_SIGN = "*"; - private final String COMMA = ","; - private final String ZERO = "0"; + private String retryError; private ArrayList waitTimes = new ArrayList<>(); @@ -32,9 +32,9 @@ class ConfigurableRetryRule { * Construct a ConfigurableRetryRule object from a String rule. * * @param rule - * The rule used to construct the ConfigRetryRule object + * the rule used to construct the ConfigRetryRule object * @throws SQLServerException - * If there is a problem parsing the rule + * if there is a problem parsing the rule */ ConfigurableRetryRule(String rule) throws SQLServerException { addElements(removeExtraElementsAndSplitRule(rule)); @@ -47,15 +47,21 @@ class ConfigurableRetryRule { * object. * * @param newRule - * The rule used to construct the ConfigRetryRule object + * the rule used to construct the ConfigRetryRule object * @param baseRule - * The ConfigRetryRule object to base the new objects off of + * the ConfigRetryRule object to base the new objects off of */ ConfigurableRetryRule(String newRule, ConfigurableRetryRule baseRule) { copyFromRule(baseRule); this.retryError = newRule; } + /** + * Copy elements from the base rule to this rule. + * + * @param baseRule + * the rule to copy elements from + */ private void copyFromRule(ConfigurableRetryRule baseRule) { this.retryError = baseRule.retryError; this.operand = baseRule.operand; @@ -70,7 +76,8 @@ private void copyFromRule(ConfigurableRetryRule baseRule) { * Removes extra elements in the rule (e.g. '{') and splits the rule based on ':' (colon). * * @param rule - * @return + * the rule to format and split + * @return the split rule as a string array */ private String[] removeExtraElementsAndSplitRule(String rule) { if (rule.endsWith(":")) { @@ -89,7 +96,7 @@ private String[] removeExtraElementsAndSplitRule(String rule) { * multi-error value, e.g. 2714,2716. This must be separated, and each error checked separately. * * @param value - * The value to be checked + * the value to be checked * @throws SQLServerException * if a non-numeric value is passed in */ @@ -140,9 +147,9 @@ private void checkParameter(String value) throws SQLServerException { * Finally, if the rule has 3 parts, it includes a query specifier, parse this and assign it. * * @param rule - * The passed in rule, as a string array + * the passed in rule, as a string array * @throws SQLServerException - * If a rule or parameter has invalid inputs + * if a rule or parameter has invalid inputs */ private void addElements(String[] rule) throws SQLServerException { if (rule.length == 2 || rule.length == 3) { @@ -220,8 +227,7 @@ private void calculateWaitTimes() { /** * Returns the retry error for this ConfigRetryRule object. * - * @return - * The retry error + * @return the retry error */ String getError() { return retryError; @@ -230,8 +236,7 @@ String getError() { /** * Returns the retry count (amount of times to retry) for this ConfigRetryRule object. * - * @return - * The retry count + * @return the retry count */ int getRetryCount() { return retryCount; @@ -240,8 +245,7 @@ int getRetryCount() { /** * Returns the retry query specifier for this ConfigRetryRule object. * - * @return - * The retry query specifier + * @return the retry query specifier */ String getRetryQueries() { return retryQueries; @@ -250,8 +254,7 @@ String getRetryQueries() { /** * Returns an array listing the waiting times between each retry, for this ConfigRetryRule object. * - * @return - * The list of waiting times + * @return the list of waiting times */ ArrayList getWaitTimes() { return waitTimes; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java index 3e8431f76..169986a0a 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/configurableretry/ConfigurableRetryLogicTest.java @@ -32,13 +32,21 @@ /** - * Test statement retry for configurable retry logic + * Test statement retry for configurable retry logic. */ public class ConfigurableRetryLogicTest extends AbstractTest { - private static String connectionStringCRL = null; - private static final String tableName = AbstractSQLGenerator + /** + * The table used throughout the tests. + */ + private static final String CRLTestTable = AbstractSQLGenerator .escapeIdentifier(RandomUtil.getIdentifier("crlTestTable")); + /** + * Sets up tests. + * + * @throws Exception + * if an exception occurs + */ @BeforeAll public static void setupTests() throws Exception { setConnection(); @@ -59,7 +67,7 @@ public void testRetryExecConnectionStringOption() throws Exception { assertTrue(test.isEmpty()); conn.setRetryExec("{2714:3,2*2:CREATE;2715:1,3}"); try { - PreparedStatement ps = conn.prepareStatement("create table " + tableName + " (c1 int null);"); + PreparedStatement ps = conn.prepareStatement("create table " + CRLTestTable + " (c1 int null);"); createTable(s); ps.execute(); Assertions.fail(TestResource.getResource("R_expectedFailPassed")); @@ -80,11 +88,10 @@ public void testRetryExecConnectionStringOption() throws Exception { */ @Test public void testStatementRetryPreparedStatement() throws Exception { - connectionStringCRL = TestUtils.addOrOverrideProperty(connectionString, "retryExec", - "{2714:3,2*2:CREATE;2715:1,3}"); - - try (Connection conn = DriverManager.getConnection(connectionStringCRL); Statement s = conn.createStatement(); - PreparedStatement ps = conn.prepareStatement("create table " + tableName + " (c1 int null);")) { + try (Connection conn = DriverManager.getConnection( + TestUtils.addOrOverrideProperty(connectionString, "retryExec", "{2714:3,2*2:CREATE;2715:1,3}")); + Statement s = conn.createStatement(); + PreparedStatement ps = conn.prepareStatement("create table " + CRLTestTable + " (c1 int null);")) { try { createTable(s); ps.execute(); @@ -106,12 +113,10 @@ public void testStatementRetryPreparedStatement() throws Exception { */ @Test public void testStatementRetryCallableStatement() throws Exception { - connectionStringCRL = TestUtils.addOrOverrideProperty(connectionString, "retryExec", - "{2714:3,2*2:CREATE;2715:1,3}"); - String call = "create table " + tableName + " (c1 int null);"; - - try (Connection conn = DriverManager.getConnection(connectionStringCRL); Statement s = conn.createStatement(); - CallableStatement cs = conn.prepareCall(call)) { + try (Connection conn = DriverManager.getConnection( + TestUtils.addOrOverrideProperty(connectionString, "retryExec", "{2714:3,2*2:CREATE;2715:1,3}")); + Statement s = conn.createStatement(); + CallableStatement cs = conn.prepareCall("create table " + CRLTestTable + " (c1 int null);")) { try { createTable(s); cs.execute(); @@ -132,12 +137,11 @@ public void testStatementRetryCallableStatement() throws Exception { * if unable to connect or execute against db */ public void testStatementRetry(String addedRetryParams) throws Exception { - String cxnString = connectionString + addedRetryParams; - - try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { + try (Connection conn = DriverManager.getConnection(connectionString + addedRetryParams); + Statement s = conn.createStatement()) { try { createTable(s); - s.execute("create table " + tableName + " (c1 int null);"); + s.execute("create table " + CRLTestTable + " (c1 int null);"); fail(TestResource.getResource("R_expectedFailPassed")); } catch (SQLServerException e) { assertTrue(e.getMessage().startsWith("There is already an object"), @@ -156,12 +160,11 @@ public void testStatementRetry(String addedRetryParams) throws Exception { * if unable to connect or execute against db */ public void testStatementRetryWithShortQueryTimeout(String addedRetryParams) throws Exception { - String cxnString = connectionString + addedRetryParams; - - try (Connection conn = DriverManager.getConnection(cxnString); Statement s = conn.createStatement()) { + try (Connection conn = DriverManager.getConnection(connectionString + addedRetryParams); + Statement s = conn.createStatement()) { try { createTable(s); - s.execute("create table " + tableName + " (c1 int null);"); + s.execute("create table " + CRLTestTable + " (c1 int null);"); fail(TestResource.getResource("R_expectedFailPassed")); } finally { dropTable(s); @@ -285,7 +288,7 @@ public void rereadAfterInterval() { } /** - * Tests that rules of the correct length, and containing valid values, pass + * Tests that rules of the correct length, and containing valid values, pass. */ @Test public void testCorrectlyFormattedRules() { @@ -420,7 +423,6 @@ public void testInitialRetryTime() throws Exception { */ @Test public void testOperand() throws Exception { - // Test incorrect try { testStatementRetry("retryExec={2714,2716:1,2AND2:CREATE};"); } catch (SQLServerException e) { @@ -436,7 +438,6 @@ public void testOperand() throws Exception { */ @Test public void testRetryChange() throws Exception { - // Test incorrect try { testStatementRetry("retryExec={2714,2716:1,2+2:CREATE};"); } catch (SQLServerException e) { @@ -444,12 +445,28 @@ public void testRetryChange() throws Exception { } } + /** + * Creates table for use in ConfigurableRetryLogic tests. + * + * @param stmt + * the SQL statement to use to create the table + * @throws SQLException + * if unable to execute statement + */ private static void createTable(Statement stmt) throws SQLException { - String sql = "create table " + tableName + " (c1 int null);"; + String sql = "create table " + CRLTestTable + " (c1 int null);"; stmt.execute(sql); } + /** + * Drops the table used in ConfigurableRetryLogic tests. + * + * @param stmt + * the SQL statement to use to drop the table + * @throws SQLException + * if unable to execute statement + */ private static void dropTable(Statement stmt) throws SQLException { - TestUtils.dropTableIfExists(tableName, stmt); + TestUtils.dropTableIfExists(CRLTestTable, stmt); } }