mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Fix orphan database connections resulting in an inability to connect to databases. #5567
This commit is contained in:
parent
22cc658dca
commit
6ae91592d1
@ -837,6 +837,13 @@ ENABLE_BINARY_PATH_BROWSING = False
|
|||||||
#############################################################################
|
#############################################################################
|
||||||
AUTO_DISCOVER_SERVERS = True
|
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
|
# Local config settings
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -37,7 +37,7 @@ from jinja2 import select_autoescape
|
|||||||
|
|
||||||
from pgadmin.model import db, Role, Server, SharedServer, ServerGroup, \
|
from pgadmin.model import db, Role, Server, SharedServer, ServerGroup, \
|
||||||
User, Keys, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
|
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.preferences import Preferences
|
||||||
from pgadmin.utils.session import create_session_interface, pga_unauthorised
|
from pgadmin.utils.session import create_session_interface, pga_unauthorised
|
||||||
from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader
|
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.security_headers import SecurityHeaders
|
||||||
from pgadmin.utils.constants import KERBEROS, OAUTH2, INTERNAL, LDAP, WEBSERVER
|
from pgadmin.utils.constants import KERBEROS, OAUTH2, INTERNAL, LDAP, WEBSERVER
|
||||||
|
|
||||||
|
|
||||||
# Explicitly set the mime-types so that a corrupted windows registry will not
|
# 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
|
# 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".
|
# 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)
|
driver.init_app(app)
|
||||||
authenticate.init_app(app)
|
authenticate.init_app(app)
|
||||||
|
heartbeat.init_app(app)
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# Register language to the preferences after login
|
# Register language to the preferences after login
|
||||||
|
@ -626,7 +626,8 @@ def utils():
|
|||||||
is_admin=current_user.has_role("Administrator"),
|
is_admin=current_user.has_role("Administrator"),
|
||||||
login_url=login_url,
|
login_url=login_url,
|
||||||
username=current_user.username,
|
username=current_user.username,
|
||||||
auth_source=auth_source
|
auth_source=auth_source,
|
||||||
|
heartbeat_timeout=config.SERVER_HEARTBEAT_TIMEOUT
|
||||||
),
|
),
|
||||||
200, {'Content-Type': MIMETYPE_APP_JS})
|
200, {'Content-Type': MIMETYPE_APP_JS})
|
||||||
|
|
||||||
|
@ -786,6 +786,10 @@ define('pgadmin.node.server', [
|
|||||||
if(data.errmsg) {
|
if(data.errmsg) {
|
||||||
Notify.error(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) {
|
.fail(function(xhr, status, error) {
|
||||||
|
@ -15,6 +15,7 @@ import Notify, {initializeModalProvider, initializeNotifier} from '../../../stat
|
|||||||
import { checkMasterPassword } from '../../../static/js/Dialogs/index';
|
import { checkMasterPassword } from '../../../static/js/Dialogs/index';
|
||||||
import { pgHandleItemError } from '../../../static/js/utils';
|
import { pgHandleItemError } from '../../../static/js/utils';
|
||||||
import { Search } from './quick_search/trigger_search';
|
import { Search } from './quick_search/trigger_search';
|
||||||
|
import { send_heartbeat } from './heartbeat';
|
||||||
|
|
||||||
define('pgadmin.browser', [
|
define('pgadmin.browser', [
|
||||||
'sources/gettext', 'sources/url_for', 'require', 'jquery',
|
'sources/gettext', 'sources/url_for', 'require', 'jquery',
|
||||||
@ -581,6 +582,10 @@ define('pgadmin.browser', [
|
|||||||
.fail(function() {/*This is intentional (SonarQube)*/});
|
.fail(function() {/*This is intentional (SonarQube)*/});
|
||||||
}, 300000);
|
}, 300000);
|
||||||
|
|
||||||
|
obj.Events.on(
|
||||||
|
'pgadmin:server:connected', send_heartbeat.bind(obj)
|
||||||
|
);
|
||||||
|
|
||||||
obj.set_master_password('');
|
obj.set_master_password('');
|
||||||
obj.check_corrupted_db_file();
|
obj.check_corrupted_db_file();
|
||||||
obj.Events.on('pgadmin:browser:tree:add', obj.onAddTreeNode.bind(obj));
|
obj.Events.on('pgadmin:browser:tree:add', obj.onAddTreeNode.bind(obj));
|
||||||
|
31
web/pgadmin/browser/static/js/heartbeat.js
Normal file
31
web/pgadmin/browser/static/js/heartbeat.js
Normal 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);
|
||||||
|
}
|
@ -66,6 +66,9 @@ define('pgadmin.browser.utils',
|
|||||||
/* GET the pgadmin server's locale */
|
/* GET the pgadmin server's locale */
|
||||||
pgAdmin['pgadmin_server_locale'] = '{{pgadmin_server_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
|
// Define list of nodes on which Query tool option doesn't appears
|
||||||
let unsupported_nodes = pgAdmin.unsupported_nodes = [
|
let unsupported_nodes = pgAdmin.unsupported_nodes = [
|
||||||
'server_group', 'server', 'coll-tablespace', 'tablespace',
|
'server_group', 'server', 'coll-tablespace', 'tablespace',
|
||||||
|
@ -19,6 +19,7 @@ from pgadmin.utils.session import cleanup_session_files
|
|||||||
from pgadmin.misc.themes import get_all_themes
|
from pgadmin.misc.themes import get_all_themes
|
||||||
from pgadmin.utils.constants import MIMETYPE_APP_JS, UTILITIES_ARRAY
|
from pgadmin.utils.constants import MIMETYPE_APP_JS, UTILITIES_ARRAY
|
||||||
from pgadmin.utils.ajax import precondition_required, make_json_response
|
from pgadmin.utils.ajax import precondition_required, make_json_response
|
||||||
|
from pgadmin.utils.heartbeat import log_server_heartbeat, get_server_heartbeat
|
||||||
import config
|
import config
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
@ -93,7 +94,8 @@ class MiscModule(PgAdminModule):
|
|||||||
list: a list of url endpoints exposed to the client.
|
list: a list of url endpoints exposed to the client.
|
||||||
"""
|
"""
|
||||||
return ['misc.ping', 'misc.index', 'misc.cleanup',
|
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):
|
def register(self, app, options):
|
||||||
"""
|
"""
|
||||||
@ -156,6 +158,29 @@ def cleanup():
|
|||||||
return ""
|
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")
|
@blueprint.route("/explain/explain.js")
|
||||||
def explain_js():
|
def explain_js():
|
||||||
"""
|
"""
|
||||||
|
@ -30,7 +30,5 @@ def init_app(app):
|
|||||||
|
|
||||||
|
|
||||||
def ping():
|
def ping():
|
||||||
drivers = getattr(current_app, '_pgadmin_server_drivers', None)
|
for type in DriverRegistry._registry:
|
||||||
|
DriverRegistry._objects[type].gc_timeout()
|
||||||
for type in drivers:
|
|
||||||
drivers[type].gc_timeout()
|
|
||||||
|
107
web/pgadmin/utils/heartbeat.py
Normal file
107
web/pgadmin/utils/heartbeat.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user