-
Notifications
You must be signed in to change notification settings - Fork 426
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Configurable Retry Logic I - Statement Retry (#2396)
* Trying on a new branch b/c I keep getting build failures and I have no idea why... * Adding back retryExec * More * Missing null check * Next to final * Removed mssql-jdbc.properties * Set up should start fresh + remove passwords to pass on pipeline * Minor cleanup * Minor cleanup * Another missing null check * Fix for timeout tests * Added timing tests + test comments * Formatting * Added a multiple rules test * Trying on a new branch b/c I keep getting build failures and I have no idea why... * More changes * Undo LimitEscapeTest changes * Remove redundant files * Final? * Remove mssql-jdpc.properties file * sync --> lock * Remove problematic test * Since error is unclear, try removing last test * Adding back connection test * I need debugging * Fix for MI * if condition for min time assertion * Leftover debug code, cleanup * Mistaken changes committed * More liberal time windows * Remove connection part * Missed some parts where connection retry was still included. * Forgot one more part * Added (most) PR comment revisions. * Add comments for specified and public facing methods * Added a missing test * More tests * Added more missing tests * Resolve retryCount test failure * Remove eaten exceptions * Removed the file not found exception as we read for file in all cases, not just when using CRL * Added a proper file read * Delete mssql-jdbc.properties * Added more coverage and minor fixes, ready for review again * Fixed read file test * Addressed recent pr comments * Remove double locking * Remove unneeded variable * Revisions after PR review * PR review update * Rename R_AKVURLInvalid as its use is no longer AKV specific * Add back logging * Typo * Removed unneeded comment * Make static variables thread-safe * Timing * JavaDoc cleanup.
- Loading branch information
1 parent
3be298f
commit 4ec4d3b
Showing
14 changed files
with
1,160 additions
and
19 deletions.
There are no files selected for viewing
292 changes: 292 additions & 0 deletions
292
src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,292 @@ | ||
/* | ||
* 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; | ||
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; | ||
import java.util.Collections; | ||
import java.util.Date; | ||
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; | ||
|
||
|
||
/** | ||
* 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; | ||
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 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"; | ||
/** | ||
* 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); | ||
/** | ||
* The last query executed (used when rule is process-dependent). | ||
*/ | ||
private static final AtomicReference<String> lastQuery = new AtomicReference<>(""); | ||
/** | ||
* The previously read rules from the connection string. | ||
*/ | ||
private static final AtomicReference<String> prevRulesFromConnectionString = new AtomicReference<>(""); | ||
/** | ||
* The list of statement retry rules. | ||
*/ | ||
private static final AtomicReference<HashMap<Integer, ConfigurableRetryRule>> 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. | ||
* | ||
* @return the static instance of ConfigurableRetryLogic | ||
* @throws SQLServerException | ||
* an exception | ||
*/ | ||
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(); | ||
} | ||
} else { | ||
refreshRuleSet(); | ||
} | ||
|
||
return singleInstance; | ||
} | ||
|
||
/** | ||
* 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.get()) >= INTERVAL_BETWEEN_READS_IN_MS) { | ||
timeLastRead.set(currentTime); | ||
if (timeLastModified.get() != 0) { | ||
// 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); | ||
} | ||
} else { | ||
setUpRules(prevRulesFromConnectionString.get()); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* 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(); | ||
} | ||
|
||
/** | ||
* 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 { | ||
LinkedList<String> temp; | ||
|
||
stmtRules.set(new HashMap<>()); | ||
lastQuery.set(""); | ||
|
||
if (cxnStrRules == null || cxnStrRules.isEmpty()) { | ||
temp = readFromFile(); | ||
} else { | ||
temp = new LinkedList<>(); | ||
Collections.addAll(temp, cxnStrRules.split(SEMI_COLON)); | ||
} | ||
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<String> listOfRules) throws SQLServerException { | ||
stmtRules.set(new HashMap<>()); | ||
|
||
for (String potentialRule : listOfRules) { | ||
ConfigurableRetryRule rule = new ConfigurableRetryRule(potentialRule); | ||
|
||
if (rule.getError().contains(COMMA)) { | ||
String[] arr = rule.getError().split(COMMA); | ||
|
||
for (String retryError : arr) { | ||
ConfigurableRetryRule splitRule = new ConfigurableRetryRule(retryError, rule); | ||
stmtRules.get().put(Integer.parseInt(splitRule.getError()), splitRule); | ||
} | ||
} else { | ||
stmtRules.get().put(Integer.parseInt(rule.getError()), rule); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* 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 = ""; | ||
|
||
try { | ||
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() + DEFAULT_PROPS_FILE; // For now, we only allow "mssql-jdbc.properties" as file name. | ||
} catch (URISyntaxException e) { | ||
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) { | ||
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UnableToFindClass")); | ||
Object[] msgArgs = {className}; | ||
throw new SQLServerException(form.format(msgArgs), null, 0, e); | ||
} | ||
} | ||
|
||
/** | ||
* Attempts to read rules from the properties file. | ||
* | ||
* @return the list of rules as a LinkedList<String> | ||
* @throws SQLServerException | ||
* if unable to read from the file | ||
*/ | ||
private static LinkedList<String> readFromFile() throws SQLServerException { | ||
String filePath = getCurrentClassPath(); | ||
LinkedList<String> list = new LinkedList<>(); | ||
|
||
try { | ||
File f = new File(filePath); | ||
try (BufferedReader buffer = new BufferedReader(new FileReader(f))) { | ||
String readLine; | ||
while ((readLine = buffer.readLine()) != null) { | ||
if (readLine.startsWith(RETRY_EXEC)) { | ||
String value = readLine.split(EQUALS_SIGN)[1]; | ||
Collections.addAll(list, value.split(SEMI_COLON)); | ||
} | ||
} | ||
} | ||
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. | ||
if (CONFIGURABLE_RETRY_LOGGER.isLoggable(java.util.logging.Level.FINER)) { | ||
CONFIGURABLE_RETRY_LOGGER.finest("File not found at path - \"" + filePath + "\""); | ||
} | ||
} catch (IOException e) { | ||
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorReadingStream")); | ||
Object[] msgArgs = {e.getMessage() + ", from path - \"" + filePath + "\""}; | ||
throw new SQLServerException(form.format(msgArgs), null, 0, e); | ||
} | ||
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<Integer, ConfigurableRetryRule> entry : stmtRules.get().entrySet()) { | ||
if (entry.getKey() == ruleToSearchFor) { | ||
return entry.getValue(); | ||
} | ||
} | ||
return null; | ||
} | ||
} |
Oops, something went wrong.