From 4d30cd3d23f297e9f8dbf931231fd428dd1c5e6b Mon Sep 17 00:00:00 2001 From: Sergey Petrunin Date: Wed, 20 Jun 2018 16:54:27 -0400 Subject: [PATCH 1/5] Adding the ability to set failover host which would be tried on original host failure. --- sql_server/pyodbc/base.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/sql_server/pyodbc/base.py b/sql_server/pyodbc/base.py index 1b0e813e..d28bc8fd 100644 --- a/sql_server/pyodbc/base.py +++ b/sql_server/pyodbc/base.py @@ -170,6 +170,12 @@ class DatabaseWrapper(BaseDatabaseWrapper): '49919', '49920', ) + _unrecoverable_error_numbers = ( + '18486', # account is locked + '18487', # password expired + '18488', # password should be changed + '18452', # login from untrusted domain + ) def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) @@ -298,24 +304,49 @@ def get_new_connection(self, conn_params): if options.get('extra_params', None): connstr += ';' + options['extra_params'] + failover_host = options.get('failover_partner', None) + failover_connstr = None + if failover_host: + failover_connstr = connstr.replace(host, failover_host, count=1) + unicode_results = options.get('unicode_results', False) timeout = options.get('connection_timeout', 0) retries = options.get('connection_retries', 5) backoff_time = options.get('connection_retry_backoff_time', 5) + failover_backoff_time = options.get('connection_retry_failover_backoff_time', 0) query_timeout = options.get('query_timeout', 0) conn = None retry_count = 0 need_to_retry = False + failover = False + error_numbers, failover_error_numbers = '', '' while conn is None: try: conn = Database.connect(connstr, unicode_results=unicode_results, timeout=timeout) except Exception as e: + current_error_numbers = e.args[1] + for error_number in self._unrecoverable_error_numbers: # never retry upon receiving unrecoverable code + if error_number in current_error_numbers: + raise + + if not failover: + error_numbers = current_error_numbers + else: + failover_error_numbers = current_error_numbers + + if failover_connstr: # retry with failover if available + connstr, failover_connstr = failover_connstr, connstr + failover = not failover + if failover: + time.sleep(failover_backoff_time) + continue + for error_number in self._transient_error_numbers: - if error_number in e.args[1]: - if error_number in e.args[1] and retry_count < retries: + if error_number in error_numbers or error_number in failover_error_numbers: + if retry_count < retries: time.sleep(backoff_time) need_to_retry = True retry_count = retry_count + 1 From 6db4651925220bf2af58bc76f57d124874cc06cc Mon Sep 17 00:00:00 2001 From: Sergey Petrunin Date: Wed, 20 Jun 2018 17:04:10 -0400 Subject: [PATCH 2/5] Refactor creating connection string into a separate function. --- sql_server/pyodbc/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sql_server/pyodbc/base.py b/sql_server/pyodbc/base.py index d28bc8fd..6673acf2 100644 --- a/sql_server/pyodbc/base.py +++ b/sql_server/pyodbc/base.py @@ -236,7 +236,7 @@ def get_connection_params(self): conn_params['NAME'] = 'master' return conn_params - def get_new_connection(self, conn_params): + def _get_connection_strings(self, conn_params, options): database = conn_params['NAME'] host = conn_params.get('HOST', 'localhost') user = conn_params.get('USER', None) @@ -244,7 +244,6 @@ def get_new_connection(self, conn_params): port = conn_params.get('PORT', None) default_driver = 'SQL Server' if os.name == 'nt' else 'FreeTDS' - options = conn_params.get('OPTIONS', {}) driver = options.get('driver', default_driver) dsn = options.get('dsn', None) @@ -309,6 +308,13 @@ def get_new_connection(self, conn_params): if failover_host: failover_connstr = connstr.replace(host, failover_host, count=1) + return connstr, failover_connstr + + def get_new_connection(self, conn_params): + options = conn_params.get('OPTIONS', {}) + + connstr, failover_connstr = self._get_connection_strings(conn_params, options) + unicode_results = options.get('unicode_results', False) timeout = options.get('connection_timeout', 0) retries = options.get('connection_retries', 5) From c2d02c964f67a4668f2f2c560bdfda53b393adac Mon Sep 17 00:00:00 2001 From: Sergey Petrunin Date: Wed, 20 Jun 2018 17:43:47 -0400 Subject: [PATCH 3/5] Bump the version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3bffa1e1..a58f3ede 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='django-pyodbc-azure', - version='1.11.13.1', + version='1.11.13.1.post1', description='Django backend for Microsoft SQL Server and Azure SQL Database using pyodbc', long_description=open('README.rst').read(), author='Michiya Takahashi', From 008626289543aad1e0b4231b8df533b1933ae11f Mon Sep 17 00:00:00 2001 From: Sergey Petrunin Date: Wed, 20 Jun 2018 18:02:25 -0400 Subject: [PATCH 4/5] Fix keyword argument for str.replace(), bump version --- setup.py | 2 +- sql_server/pyodbc/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a58f3ede..dd8ecbb6 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='django-pyodbc-azure', - version='1.11.13.1.post1', + version='1.11.13.1.post2', description='Django backend for Microsoft SQL Server and Azure SQL Database using pyodbc', long_description=open('README.rst').read(), author='Michiya Takahashi', diff --git a/sql_server/pyodbc/base.py b/sql_server/pyodbc/base.py index 6673acf2..0fcb6793 100644 --- a/sql_server/pyodbc/base.py +++ b/sql_server/pyodbc/base.py @@ -306,7 +306,7 @@ def _get_connection_strings(self, conn_params, options): failover_host = options.get('failover_partner', None) failover_connstr = None if failover_host: - failover_connstr = connstr.replace(host, failover_host, count=1) + failover_connstr = connstr.replace(host, failover_host, 1) return connstr, failover_connstr From 6ae77ed4f3862f9ac63ffb00d8b5a644b0e99034 Mon Sep 17 00:00:00 2001 From: Sergey Petrunin Date: Thu, 21 Jun 2018 14:24:31 -0400 Subject: [PATCH 5/5] Remove version, as I'm not sure to what to set it. Add new options description in README.rst --- README.rst | 13 ++++++++++++- setup.py | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index e0effe09..5f54235a 100644 --- a/README.rst +++ b/README.rst @@ -199,7 +199,7 @@ Dictionary. Current available keys are: - connection_retry_backoff_time - Integer. Sets the back off time in seconds for reries of + Integer. Sets the back off time in seconds for retries of the database connection process. Default value is ``5``. - query_timeout @@ -207,6 +207,17 @@ Dictionary. Current available keys are: Integer. Sets the timeout in seconds for the database query. Default value is ``0`` which disables the timeout. +- failover_partner + + String. Same as HOST but for failover partner. + Default is not specified which disable the failover partner. + +- connection_retry_failover_backoff_time + + Integer. Sets the back off time in seconds before trying + to connect to failover partner. + Default value is ``0`` which disables the timeout. + backend-specific settings ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/setup.py b/setup.py index dd8ecbb6..3bffa1e1 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='django-pyodbc-azure', - version='1.11.13.1.post2', + version='1.11.13.1', description='Django backend for Microsoft SQL Server and Azure SQL Database using pyodbc', long_description=open('README.rst').read(), author='Michiya Takahashi',