Skip to content

Commit

Permalink
Configurable Retry Logic I - Statement Retry (#2396)
Browse files Browse the repository at this point in the history
* 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
Jeffery-Wasty authored Sep 23, 2024
1 parent 3be298f commit 4ec4d3b
Show file tree
Hide file tree
Showing 14 changed files with 1,160 additions and 19 deletions.
292 changes: 292 additions & 0 deletions src/main/java/com/microsoft/sqlserver/jdbc/ConfigurableRetryLogic.java
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;
}
}
Loading

0 comments on commit 4ec4d3b

Please sign in to comment.