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
|
||||
|
||||
#############################################################################
|
||||
# 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
|
||||
##########################################################################
|
||||
|
@ -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
|
||||
|
@ -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})
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
|
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 */
|
||||
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',
|
||||
|
@ -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():
|
||||
"""
|
||||
|
@ -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()
|
||||
|
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