Handling the bad/lost connection of a database server.

Made backend changes for:
* Taking care of the connection status in the psycopg2 driver. And, when
  the connection is lost, it throws a exception with 503 http status
  message, and connection lost information in it.
* Allowing the flask application to propagate the exceptions even in the
  release mode.
* Utilising the existing password (while reconnection, if not
  disconnected explicitly).
* Introduced a new ajax response message 'service_unavailable' (http
  status code: 503), which suggests temporary service unavailable.

Client (front-end) changes:
* To handle the connection lost of a database server for different
  operations by generating proper events, and handle them properly.

Removed the connection status check code from different nodes, so that
- it generates the proper exception, when accessing the non-alive
  connection.

Fixes #1387
This commit is contained in:
Ashesh Vashi
2016-08-29 12:01:35 +05:30
parent 1b05464a04
commit f12d981a9d
51 changed files with 1235 additions and 970 deletions
+22 -6
View File
@@ -30,8 +30,9 @@ class DataTypeJSONEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, obj)
def make_json_response(success=1, errormsg='', info='', result=None,
data=None, status=200):
def make_json_response(
success=1, errormsg='', info='', result=None, data=None, status=200
):
"""Create a HTML response document describing the results of a request and
containing the data."""
doc = dict()
@@ -44,7 +45,7 @@ def make_json_response(success=1, errormsg='', info='', result=None,
return Response(
response=json.dumps(doc, cls=DataTypeJSONEncoder),
status=status,
mimetype="text/json"
mimetype="application/json"
)
@@ -53,7 +54,7 @@ def make_response(response=None, status=200):
return Response(
response=json.dumps(response, cls=DataTypeJSONEncoder),
status=status,
mimetype="text/json"
mimetype="application/json"
)
@@ -120,10 +121,25 @@ def gone(errormsg=''):
)
def not_implemented(errormsg=_('Not implemented.')):
def not_implemented(errormsg=_('Not implemented.'), info='', result=None, data=None):
"""Create a response with HTTP status code 501 - Not Implemented."""
return make_json_response(
status=501,
success=0,
errormsg=errormsg
errormsg=errormsg,
info=info,
result=result,
data=data
)
def service_unavailable(errormsg=_("Service Unavailable"), info='', result=None, data=None):
"""Create a response with HTTP status code 503 - Server Unavailable."""
return make_json_response(
status=503,
success=0,
errormsg=errormsg,
info=info,
result=result,
data=data
)
+60 -14
View File
@@ -30,6 +30,7 @@ from psycopg2.extensions import adapt
import config
from pgadmin.model import Server, User
from pgadmin.utils.exception import ConnectionLost
from .keywords import ScanKeyword
from ..abstract import BaseDriver, BaseConnection
from .cursor import DictCursor
@@ -179,6 +180,7 @@ class Connection(BaseConnection):
self.execution_aborted = False
self.row_count = 0
self.__notices = None
self.password = None
super(Connection, self).__init__()
@@ -225,10 +227,13 @@ class Connection(BaseConnection):
password = None
mgr = self.manager
if 'password' in kwargs:
encpass = kwargs['password']
else:
encpass = getattr(mgr, 'password', None)
encpass = kwargs['password'] if 'password' in kwargs else None
if encpass is None:
encpass = self.password or getattr(mgr, 'password', None)
# Reset the existing connection password
self.password = None
if encpass:
# Fetch Logged in User Details.
@@ -239,6 +244,10 @@ class Connection(BaseConnection):
try:
password = decrypt(encpass, user.password)
# password is in bytes, for python3 we need it in string
if isinstance(password, bytes):
password = password.decode()
except Exception as e:
current_app.logger.exception(e)
return False, \
@@ -246,10 +255,6 @@ class Connection(BaseConnection):
str(e)
)
# password is in bytes, for python3 we need it in string
if isinstance(password, bytes):
password = password.decode()
try:
if hasattr(str, 'decode'):
database = self.db.encode('utf-8')
@@ -401,11 +406,18 @@ WHERE
mgr.server_cls = st
break
mgr._update_password(encpass)
mgr.update_session()
return True, None
def __cursor(self, server_cursor=False):
if not self.conn:
raise ConnectionLost(
self.manager.sid,
self.db,
None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
)
cur = getattr(g, "{0}#{1}".format(
self.manager.sid,
self.conn_id.encode('utf-8')
@@ -475,7 +487,7 @@ Attempting to reconnect to the database server (#{server_id}) for the connection
Connection for server#{0} with database "{1}" was lost.
Attempt to reconnect it failed with the error:
{2}"""
).format(self.driver.server_id, self.database, cur)
).format(self.driver.server_id, self.db, cur)
current_app.logger.error(msg)
return False, cur
@@ -593,6 +605,12 @@ Attempt to reconnect it failed with the error:
self.__internal_blocking_execute(cur, query, params)
except psycopg2.Error as pe:
cur.close()
if not self.connected():
raise ConnectionLost(
self.manager.sid,
self.db,
None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
)
errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
current_app.logger.error(
u"Failed to execute query (execute_scalar) for the server #{server_id} - {conn_id} (Query-id: {query_id}):\nError Message:{errmsg}".format(
@@ -694,6 +712,12 @@ Failed to execute query (execute_async) for the server #{server_id} - {conn_id}
self.__internal_blocking_execute(cur, query, params)
except psycopg2.Error as pe:
cur.close()
if not self.connected():
raise ConnectionLost(
self.manager.sid,
self.db,
None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
)
errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
current_app.logger.error(u"""
Failed to execute query (execute_void) for the server #{server_id} - {conn_id}
@@ -733,6 +757,12 @@ Failed to execute query (execute_void) for the server #{server_id} - {conn_id}
self.__internal_blocking_execute(cur, query, params)
except psycopg2.Error as pe:
cur.close()
if not self.connected():
raise ConnectionLost(
self.manager.sid,
self.db,
None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
)
errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
current_app.logger.error(
u"Failed to execute query (execute_2darray) for the server #{server_id} - {conn_id} (Query-id: {query_id}):\nError Message:{errmsg}".format(
@@ -778,6 +808,12 @@ Failed to execute query (execute_void) for the server #{server_id} - {conn_id}
self.__internal_blocking_execute(cur, query, params)
except psycopg2.Error as pe:
cur.close()
if not self.connected():
raise ConnectionLost(
self.manager.sid,
self.db,
None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
)
errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
current_app.logger.error(
u"Failed to execute query (execute_dict) for the server #{server_id}- {conn_id} (Query-id: {query_id}):\nError Message:{errmsg}".format(
@@ -867,6 +903,7 @@ Failed to reset the connection to the server due to following error:
if self.conn:
self.conn.close()
self.conn = None
self.password = None
def _wait(self, conn):
"""
@@ -942,6 +979,12 @@ Failed to reset the connection to the server due to following error:
try:
status = self._wait_timeout(self.conn, ASYNC_WAIT_TIMEOUT)
except psycopg2.Error as pe:
if cur.closed:
raise ConnectionLost(
self.manager.sid,
self.db,
self.conn_id[5:]
)
errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
return False, errmsg, None
@@ -1242,10 +1285,6 @@ class ServerManager(object):
self, database=None, conn_id=None, auto_reconnect=True, did=None,
async=None
):
msg_active_conn = gettext(
"Server has no active connection. Please connect to the server."
)
if database is not None:
if hasattr(str, 'decode') and \
not isinstance(database, unicode):
@@ -1285,7 +1324,7 @@ WHERE db.oid = {0}""".format(did))
))
if database is None:
raise Exception(msg_active_conn)
raise ConnectionLost(self.sid, None, None)
my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
(u'DB:{0}'.format(database))
@@ -1387,6 +1426,13 @@ WHERE db.oid = {0}""".format(did))
return True
def _update_password(self, passwd):
self.password = passwd
for conn_id in self.connections:
conn = self.connections[conn_id]
if conn.conn is not None:
conn.password = passwd
def update_session(self):
managers = session['__pgsql_server_managers'] \
if '__pgsql_server_managers' in session else dict()
+42
View File
@@ -0,0 +1,42 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from werkzeug.exceptions import HTTPException
from werkzeug.http import HTTP_STATUS_CODES
from flask_babel import gettext as _
from flask import request
from pgadmin.utils.ajax import service_unavailable
class ConnectionLost(HTTPException):
"""
Exception
"""
def __init__(self, _server_id, _database_name, _conn_id):
self.sid = _server_id
self.db = _database_name
self.conn_id = _conn_id
HTTPException.__init__(self)
@property
def name(self):
return HTTP_STATUS_CODES.get(505, 'Service Unavailable')
def get_response(self, environ=None):
return service_unavailable(
_("Connection to the server has been lost!"),
info="CONNECTION_LOST",
data={
'sid': self.sid,
'database': self.db,
'conn_id': self.conn_id
}
)