Make the session thread safe.

As sessions in pgAdmin4 are filesystem based session, they need locking
for avoiding the access from multiple threads, specially running as an
WSGI application.

Fixes #3547
This commit is contained in:
Harshal Dhumal 2018-08-22 11:55:39 +05:30 committed by Ashesh Vashi
parent 70c95fcdd5
commit 013ad7446f
2 changed files with 44 additions and 33 deletions

View File

@ -20,5 +20,6 @@ Bug fixes
| `Bug #3407 <https://redmine.postgresql.org/issues/3407>`_ - Fix keyboard shortcuts layout in the preferences panel. | `Bug #3407 <https://redmine.postgresql.org/issues/3407>`_ - Fix keyboard shortcuts layout in the preferences panel.
| `Bug #3461 <https://redmine.postgresql.org/issues/3461>`_ - Ensure that refreshing a node also updates the Property list. | `Bug #3461 <https://redmine.postgresql.org/issues/3461>`_ - Ensure that refreshing a node also updates the Property list.
| `Bug #3528 <https://redmine.postgresql.org/issues/3528>`_ - Handle connection errors properly in the query tool. | `Bug #3528 <https://redmine.postgresql.org/issues/3528>`_ - Handle connection errors properly in the query tool.
| `Bug #3547 <https://redmine.postgresql.org/issues/3578>`_ - Make session implementation thread safe
| `Bug #3558 <https://redmine.postgresql.org/issues/3558>`_ - Fix sort/filter dialog editing issue. | `Bug #3558 <https://redmine.postgresql.org/issues/3558>`_ - Fix sort/filter dialog editing issue.
| `Bug #3578 <https://redmine.postgresql.org/issues/3578>`_ - Ensure sql for Role should be visible in SQL panel for GPDB. | `Bug #3578 <https://redmine.postgresql.org/issues/3578>`_ - Ensure sql for Role should be visible in SQL panel for GPDB.

View File

@ -24,6 +24,7 @@ import random
import string import string
import time import time
from uuid import uuid4 from uuid import uuid4
from threading import Lock
from flask import current_app, request, flash, redirect from flask import current_app, request, flash, redirect
from flask_login import login_url from flask_login import login_url
from pgadmin.utils.ajax import make_json_response from pgadmin.utils.ajax import make_json_response
@ -50,6 +51,9 @@ def _calc_hmac(body, secret):
).decode() ).decode()
sess_lock = Lock()
class ManagedSession(CallbackDict, SessionMixin): class ManagedSession(CallbackDict, SessionMixin):
def __init__(self, initial=None, sid=None, new=False, randval=None, def __init__(self, initial=None, sid=None, new=False, randval=None,
hmac_digest=None): hmac_digest=None):
@ -111,8 +115,9 @@ class CachingSessionManager(SessionManager):
def _normalize(self): def _normalize(self):
if len(self._cache) > self.num_to_store: if len(self._cache) > self.num_to_store:
# Flush 20% of the cache # Flush 20% of the cache
while len(self._cache) > (self.num_to_store * 0.8): with sess_lock:
self._cache.popitem(False) while len(self._cache) > (self.num_to_store * 0.8):
self._cache.popitem(False)
def new_session(self): def new_session(self):
session = self.parent.new_session() session = self.parent.new_session()
@ -122,59 +127,64 @@ class CachingSessionManager(SessionManager):
if request.path.startswith(sp): if request.path.startswith(sp):
return session return session
self._cache[session.sid] = session with sess_lock:
self._cache[session.sid] = session
self._normalize() self._normalize()
return session return session
def remove(self, sid): def remove(self, sid):
self.parent.remove(sid) with sess_lock:
if sid in self._cache: self.parent.remove(sid)
del self._cache[sid] if sid in self._cache:
del self._cache[sid]
def exists(self, sid): def exists(self, sid):
if sid in self._cache: with sess_lock:
return True if sid in self._cache:
return self.parent.exists(sid) return True
return self.parent.exists(sid)
def get(self, sid, digest): def get(self, sid, digest):
session = None session = None
if sid in self._cache: with sess_lock:
session = self._cache[sid] if sid in self._cache:
if session.hmac_digest != digest: session = self._cache[sid]
session = None if session.hmac_digest != digest:
session = None
# reset order in Dict # reset order in Dict
del self._cache[sid] del self._cache[sid]
if not session: if not session:
session = self.parent.get(sid, digest) session = self.parent.get(sid, digest)
# Do not store the session if skip paths # Do not store the session if skip paths
for sp in self.skip_paths: for sp in self.skip_paths:
if request.path.startswith(sp): if request.path.startswith(sp):
return session return session
self._cache[sid] = session self._cache[sid] = session
self._normalize() self._normalize()
return session return session
def put(self, session): def put(self, session):
self.parent.put(session) with sess_lock:
self.parent.put(session)
# Do not store the session if skip paths # Do not store the session if skip paths
for sp in self.skip_paths: for sp in self.skip_paths:
if request.path.startswith(sp): if request.path.startswith(sp):
return return
if session.sid in self._cache: if session.sid in self._cache:
try: try:
del self._cache[session.sid] del self._cache[session.sid]
except Exception: except Exception:
pass pass
self._cache[session.sid] = session self._cache[session.sid] = session
self._normalize() self._normalize()