Fix orphan database connections resulting in an inability to connect to databases. #5567

This commit is contained in:
Khushboo Vashi 2023-01-19 15:57:02 +05:30 committed by GitHub
parent 22cc658dca
commit 6ae91592d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 190 additions and 7 deletions

View File

@ -837,6 +837,13 @@ ENABLE_BINARY_PATH_BROWSING = False
#############################################################################
AUTO_DISCOVER_SERVERS = True
#############################################################################
# SERVER_HEARTBEAT_TIMEOUT is used to send the server heartbeat to server
# from the client. This will resolve the orphan database issue once
# browser tab is closed.
#############################################################################
SERVER_HEARTBEAT_TIMEOUT = 30 # In seconds
##########################################################################
# Local config settings
##########################################################################

View File

@ -37,7 +37,7 @@ from jinja2 import select_autoescape
from pgadmin.model import db, Role, Server, SharedServer, ServerGroup, \
User, Keys, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
from pgadmin.utils import PgAdminModule, driver, KeyManager
from pgadmin.utils import PgAdminModule, driver, KeyManager, heartbeat
from pgadmin.utils.preferences import Preferences
from pgadmin.utils.session import create_session_interface, pga_unauthorised
from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader
@ -49,6 +49,7 @@ from pgadmin import authenticate
from pgadmin.utils.security_headers import SecurityHeaders
from pgadmin.utils.constants import KERBEROS, OAUTH2, INTERNAL, LDAP, WEBSERVER
# Explicitly set the mime-types so that a corrupted windows registry will not
# affect pgAdmin 4 to be load properly. This will avoid the issues that may
# occur due to security fix of X_CONTENT_TYPE_OPTIONS = "nosniff".
@ -541,6 +542,7 @@ def create_app(app_name=None):
##########################################################################
driver.init_app(app)
authenticate.init_app(app)
heartbeat.init_app(app)
##########################################################################
# Register language to the preferences after login

View File

@ -626,7 +626,8 @@ def utils():
is_admin=current_user.has_role("Administrator"),
login_url=login_url,
username=current_user.username,
auth_source=auth_source
auth_source=auth_source,
heartbeat_timeout=config.SERVER_HEARTBEAT_TIMEOUT
),
200, {'Content-Type': MIMETYPE_APP_JS})

View File

@ -786,6 +786,10 @@ define('pgadmin.node.server', [
if(data.errmsg) {
Notify.error(data.errmsg);
}
// Generate the event that server is connected
pgBrowser.Events.trigger(
'pgadmin:server:connected', data._id, item, data
);
}
})
.fail(function(xhr, status, error) {

View File

@ -15,6 +15,7 @@ import Notify, {initializeModalProvider, initializeNotifier} from '../../../stat
import { checkMasterPassword } from '../../../static/js/Dialogs/index';
import { pgHandleItemError } from '../../../static/js/utils';
import { Search } from './quick_search/trigger_search';
import { send_heartbeat } from './heartbeat';
define('pgadmin.browser', [
'sources/gettext', 'sources/url_for', 'require', 'jquery',
@ -581,6 +582,10 @@ define('pgadmin.browser', [
.fail(function() {/*This is intentional (SonarQube)*/});
}, 300000);
obj.Events.on(
'pgadmin:server:connected', send_heartbeat.bind(obj)
);
obj.set_master_password('');
obj.check_corrupted_db_file();
obj.Events.on('pgadmin:browser:tree:add', obj.onAddTreeNode.bind(obj));

View File

@ -0,0 +1,31 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import getApiInstance from '../../../static/js/api_instance';
import Notifier from '../../../static/js/helpers/Notifier';
import pgAdmin from 'sources/pgadmin';
const axiosApi = getApiInstance();
let HEARTBEAT_TIMEOUT = pgAdmin.heartbeat_timeout * 1000;
export function send_heartbeat(_server_id) {
// Send heartbeat to the server every 30 seconds
setInterval(function() {
axiosApi.post(url_for('misc.heartbeat'), {'sid': _server_id})
.then(() => {
// pass
})
.catch((error) => {
Notifier.error(gettext(`pgAdmin server not responding, try to login again: ${error.message || error.response.data.errormsg}`));
});
}, HEARTBEAT_TIMEOUT);
}

View File

@ -66,6 +66,9 @@ define('pgadmin.browser.utils',
/* GET the pgadmin server's locale */
pgAdmin['pgadmin_server_locale'] = '{{pgadmin_server_locale}}';
/* Server Heartbeat Timeout */
pgAdmin['heartbeat_timeout'] = '{{heartbeat_timeout}}';
// Define list of nodes on which Query tool option doesn't appears
let unsupported_nodes = pgAdmin.unsupported_nodes = [
'server_group', 'server', 'coll-tablespace', 'tablespace',

View File

@ -19,6 +19,7 @@ from pgadmin.utils.session import cleanup_session_files
from pgadmin.misc.themes import get_all_themes
from pgadmin.utils.constants import MIMETYPE_APP_JS, UTILITIES_ARRAY
from pgadmin.utils.ajax import precondition_required, make_json_response
from pgadmin.utils.heartbeat import log_server_heartbeat, get_server_heartbeat
import config
import subprocess
import os
@ -93,7 +94,8 @@ class MiscModule(PgAdminModule):
list: a list of url endpoints exposed to the client.
"""
return ['misc.ping', 'misc.index', 'misc.cleanup',
'misc.validate_binary_path']
'misc.validate_binary_path', 'misc.heartbeat',
'misc.get_heartbeat']
def register(self, app, options):
"""
@ -156,6 +158,29 @@ def cleanup():
return ""
@blueprint.route("/heartbeat", methods=['POST'])
@pgCSRFProtect.exempt
def heartbeat():
data = None
if hasattr(request.data, 'decode'):
data = request.data.decode('utf-8')
if data != '':
data = json.loads(data)
log_server_heartbeat(data)
return make_json_response(data=gettext('Heartbeat logged successfully.'),
status=200)
@blueprint.route("/get_heartbeat/<int:sid>", methods=['GET'])
@pgCSRFProtect.exempt
def get_heartbeat(sid):
heartbeat_data = get_server_heartbeat(sid)
return make_json_response(data=heartbeat_data,
status=200)
@blueprint.route("/explain/explain.js")
def explain_js():
"""

View File

@ -30,7 +30,5 @@ def init_app(app):
def ping():
drivers = getattr(current_app, '_pgadmin_server_drivers', None)
for type in drivers:
drivers[type].gc_timeout()
for type in DriverRegistry._registry:
DriverRegistry._objects[type].gc_timeout()

View File

@ -0,0 +1,107 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2023, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
#########################################################################
"""Server heartbeat manager."""
import threading
import datetime
import config
from flask import session, current_app
def log_server_heartbeat(data):
"""Log Server Heartbeat."""
from config import PG_DEFAULT_DRIVER
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(int(data['sid'])
)
_server_heartbeat = getattr(current_app, '_pgadmin_server_heartbeat', {})
if session.sid not in _server_heartbeat:
_server_heartbeat[session.sid] = {}
_server_heartbeat[session.sid][data['sid']] = {
'timestamp': datetime.datetime.now(),
'conn': manager.connections
}
setattr(current_app, '_pgadmin_server_heartbeat', _server_heartbeat)
current_app.logger.debug(
"Heartbeat logged for the session id##server id: {0}##{1}".format(
session.sid, data['sid']))
def get_server_heartbeat(server_id):
_server_heartbeat = getattr(current_app, '_pgadmin_server_heartbeat', {})
if session.sid in _server_heartbeat and server_id in _server_heartbeat[
session.sid]:
return _server_heartbeat[session.sid][server_id]
else:
return None
class ServerHeartbeatTimer():
def __init__(self, sec, _app):
def func_wrapper():
self.t = threading.Timer(sec, func_wrapper)
self.t.start()
self.release_server_heartbeat()
self.t = threading.Timer(sec, func_wrapper)
self.t.start()
self._app = _app
def release_server_heartbeat(self):
with self._app.app_context():
_server_heartbeat = getattr(self._app,
'_pgadmin_server_heartbeat', {})
if len(_server_heartbeat) > 0:
for sess_id in list(_server_heartbeat):
for sid in list(_server_heartbeat[sess_id]):
last_heartbeat_time = _server_heartbeat[sess_id][sid][
'timestamp']
current_time = datetime.datetime.now()
diff = current_time - last_heartbeat_time
# Wait for 4 times then the timeout
if diff.total_seconds() > (
config.SERVER_HEARTBEAT_TIMEOUT * 4):
self._release_connections(
_server_heartbeat[sess_id][sid]['conn'],
sess_id, sid)
_server_heartbeat[sess_id].pop(sid)
if len(_server_heartbeat[sess_id]) == 0:
_server_heartbeat.pop(sess_id)
setattr(self._app, '_pgadmin_server_heartbeat',
_server_heartbeat)
@staticmethod
def _release_connections(server_conn, sess_id, sid):
for d in server_conn:
# Release the connection
server_conn[d]._release()
# Reconnect on the reload
server_conn[d].wasConnected = True
current_app.logger.debug(
"Heartbeat not received. Released "
"connection for the session "
"id##server id: {0}##{1}".format(
sess_id, sid))
def cancel(self):
self.t.cancel()
def init_app(app):
setattr(app, '_pgadmin_server_heartbeat', {})
ServerHeartbeatTimer(sec=config.SERVER_HEARTBEAT_TIMEOUT,
_app=app)