def _session(request): engine = request.registry['sqlalchemy.engine'] session = Session(bind=engine) # If the request has a transaction manager, associate the session with it. try: tm = request.tm except AttributeError: pass else: zope.sqlalchemy.register(session, transaction_manager=tm) # Track uncommitted changes so we can verify that everything was either # committed or rolled back when the request finishes. db_session_checks = request.registry.settings.get('h.db_session_checks', True) if db_session_checks: tracker = Tracker(session) else: tracker = None # pyramid_tm doesn't always close the database session for us. # # For example if an exception view accesses the session and causes a new # transaction to be opened, pyramid_tm won't close this connection because # pyramid_tm's transaction has already ended before exception views are # executed. # Connections opened by NewResponse and finished callbacks aren't closed by # pyramid_tm either. # # So add our own callback here to make sure db sessions are always closed. # # See: https://github.com/Pylons/pyramid_tm/issues/40 @request.add_finished_callback def close_the_sqlalchemy_session(request): changes = tracker.uncommitted_changes() if tracker else [] if changes: msg = 'closing a session with uncommitted changes %s' log.warn(msg, changes, extra={ 'stack': True, 'changes': changes, }) session.close() # zope.sqlalchemy maintains an internal `id(session) => state` map with # an entry for each active DB session which is registered with it. # # Entries are normally cleared at the end of a request when the # transaction manager (`request.tm`) commits. DB writes after this can # leave stale entries in the map which can cause problems in future # requests if another session gets the same ID as the current one. dm = zope.sqlalchemy.datamanager if len(dm._SESSION_STATE) > 0: log.warn('request ended with non-empty zope.sqlalchemy state', extra={ 'data': { 'zope.sqlalchemy.datamanager._SESSION_STATE': dm._SESSION_STATE, }, }) dm._SESSION_STATE = {} return session
def _session(request): engine = request.registry["sqlalchemy.engine"] session = Session(bind=engine) # If the request has a transaction manager, associate the session with it. try: tm = request.tm except AttributeError: pass else: zope.sqlalchemy.register(session, transaction_manager=tm) # Track uncommitted changes so we can verify that everything was either # committed or rolled back when the request finishes. db_session_checks = request.registry.settings.get("h.db_session_checks", True) if db_session_checks: tracker = Tracker(session) else: tracker = None # pyramid_tm doesn't always close the database session for us. # # For example if an exception view accesses the session and causes a new # transaction to be opened, pyramid_tm won't close this connection because # pyramid_tm's transaction has already ended before exception views are # executed. # Connections opened by NewResponse and finished callbacks aren't closed by # pyramid_tm either. # # So add our own callback here to make sure db sessions are always closed. # # See: https://github.com/Pylons/pyramid_tm/issues/40 @request.add_finished_callback def close_the_sqlalchemy_session(request): if len(session.transaction._connections) > 1: # There appear to still be open DB connections belonging to this # request. This shouldn't happen. changes = tracker.uncommitted_changes() if tracker else [] if changes: msg = "closing a session with uncommitted changes %s" log.warning(msg, changes, extra={ "stack": True, "changes": changes }) else: log.warning( "closing an unclosed DB session (no uncommitted changes)", extra={"stack": True}, ) # Close any unclosed DB connections. # This is done outside of the `if` statement above just in case: it's # okay to call `session.close()` even if the session does not need to # be closed, so just call it unconditionally so that there's no chance # of leaking any unclosed DB connections. session.close() return session
def _session(request): engine = request.registry["sqlalchemy.engine"] session = Session(bind=engine) # If the request has a transaction manager, associate the session with it. try: tm = request.tm except AttributeError: pass else: zope.sqlalchemy.register(session, transaction_manager=tm) # Track uncommitted changes so we can verify that everything was either # committed or rolled back when the request finishes. db_session_checks = request.registry.settings.get("h.db_session_checks", True) if db_session_checks: tracker = Tracker(session) else: tracker = None # pyramid_tm doesn't always close the database session for us. # # If anything that executes later in the Pyramid request processing cycle # than pyramid_tm tween egress opens a new DB session (for example a tween # above the pyramid_tm tween, a response callback, or a NewResponse # subscriber) then pyramid_tm won't close that DB session for us. # # So as a precaution add our own callback here to make sure db sessions are # always closed. @request.add_finished_callback def close_the_sqlalchemy_session(request): if len(session.transaction._connections) > 1: # There appear to still be open DB connections belonging to this # request. This shouldn't happen. changes = tracker.uncommitted_changes() if tracker else [] if changes: msg = "closing a session with uncommitted changes %s" log.warning(msg, changes, extra={ "stack": True, "changes": changes }) else: log.warning( "closing an unclosed DB session (no uncommitted changes)", extra={"stack": True}, ) # Close any unclosed DB connections. # This is done outside of the `if` statement above just in case: it's # okay to call `session.close()` even if the session does not need to # be closed, so just call it unconditionally so that there's no chance # of leaking any unclosed DB connections. session.close() return session
def _session(request): engine = request.registry["sqlalchemy.engine"] session = Session(bind=engine) # If the request has a transaction manager, associate the session with it. try: tm = request.tm except AttributeError: pass else: zope.sqlalchemy.register(session, transaction_manager=tm) # Track uncommitted changes so we can verify that everything was either # committed or rolled back when the request finishes. db_session_checks = request.registry.settings.get("h.db_session_checks", True) if db_session_checks: tracker = Tracker(session) else: tracker = None # pyramid_tm doesn't always close the database session for us. # # For example if an exception view accesses the session and causes a new # transaction to be opened, pyramid_tm won't close this connection because # pyramid_tm's transaction has already ended before exception views are # executed. # Connections opened by NewResponse and finished callbacks aren't closed by # pyramid_tm either. # # So add our own callback here to make sure db sessions are always closed. # # See: https://github.com/Pylons/pyramid_tm/issues/40 @request.add_finished_callback def close_the_sqlalchemy_session(request): changes = tracker.uncommitted_changes() if tracker else [] if changes: msg = "closing a session with uncommitted changes %s" log.warning(msg, changes, extra={ "stack": True, "changes": changes }) session.close() return session
def tracker(self, db_session): return Tracker(db_session)
def _session(request): engine = request.registry['sqlalchemy.engine'] session = Session(bind=engine) # If the request has a transaction manager, associate the session with it. try: tm = request.tm except AttributeError: pass else: zope.sqlalchemy.register(session, transaction_manager=tm) # Track uncommitted changes so we can verify that everything was either # committed or rolled back when the request finishes. db_session_checks = request.registry.settings.get('h.db_session_checks', True) if db_session_checks: tracker = Tracker(session) else: tracker = None # pyramid_tm doesn't always close the database session for us. # # For example if an exception view accesses the session and causes a new # transaction to be opened, pyramid_tm won't close this connection because # pyramid_tm's transaction has already ended before exception views are # executed. # Connections opened by NewResponse and finished callbacks aren't closed by # pyramid_tm either. # # So add our own callback here to make sure db sessions are always closed. # # See: https://github.com/Pylons/pyramid_tm/issues/40 @request.add_finished_callback def close_the_sqlalchemy_session(request): changes = tracker.uncommitted_changes() if tracker else [] if changes: msg = 'closing a session with uncommitted changes %s' log.warn(msg, changes, extra={ 'stack': True, 'changes': changes, }) session.close() # Remove stale sqlalchemy session IDs from zope.sqlalchemy's _SESSION_STATE. # # TODO: Once https://github.com/zopefoundation/zope.sqlalchemy/pull/23 # has been merged and we upgrade to a new version of zope.sqlalchemy # that includes it, we can remove this workaround. # # _SESSION_STATE is a dict whose keys are the Python object IDs of # sqlalchemy sessions. A session's ID is normally removed from # _SESSION_STATE at the end of processing that session's request. But # if something opens a new DB session by accessing the DB after the # transaction manager has committed then that session's ID is **never** # removed from _SESSION_STATE even after the session object has been # garbage collected. # # If a future request's session then happens to get the same Python # object ID as one of these "stale" IDs not removed from # _SESSION_STATE, then zope.sqlalchemy does not join that sqlalchemy # session to the transaction manager's transaction because it thinks it # has already done so. As a result, that session is never committed # (annotations are not saved, etc). # # To prevent that from happening we remove stale IDs from # _SESSION_STATE here. # dm = zope.sqlalchemy.datamanager if len(dm._SESSION_STATE) > 0: log.warn('request ended with non-empty zope.sqlalchemy state', extra={ 'data': { 'zope.sqlalchemy.datamanager._SESSION_STATE': dm._SESSION_STATE, }, }) dm._SESSION_STATE = {} return session