Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #34806 -- Made the cached_db backend resilient to cache backend errors. #17220

Merged
merged 1 commit into from Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion django/contrib/sessions/backends/cached_db.py
Expand Up @@ -2,12 +2,16 @@
Cached, database-backed sessions.
"""

import logging

from django.conf import settings
from django.contrib.sessions.backends.db import SessionStore as DBStore
from django.core.cache import caches

KEY_PREFIX = "django.contrib.sessions.cached_db"

logger = logging.getLogger("django.contrib.sessions")


class SessionStore(DBStore):
"""
Expand Down Expand Up @@ -52,7 +56,10 @@ def exists(self, session_key):

def save(self, must_create=False):
super().save(must_create)
self._cache.set(self.cache_key, self._session, self.get_expiry_age())
try:
self._cache.set(self.cache_key, self._session, self.get_expiry_age())
except Exception:
logger.exception("Error saving to cache (%s)", self._cache)

def delete(self, session_key=None):
super().delete(session_key)
Expand Down
11 changes: 11 additions & 0 deletions docs/ref/logging.txt
Expand Up @@ -286,6 +286,17 @@ Messages to this logger have ``params`` and ``sql`` in their extra context (but
unlike ``django.db.backends``, not duration). The values have the same meaning
as explained in :ref:`django-db-logger`.

.. _django-contrib-sessions-logger:

``django.contrib.sessions``
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Log messages related to the :doc:`session framework</topics/http/sessions>`.

* Non-fatal errors occurring when using the
:class:`django.contrib.sessions.backends.cached_db.SessionStore` engine are
logged as ``ERROR`` messages with the corresponding traceback.

Handlers
--------

Expand Down
5 changes: 4 additions & 1 deletion docs/releases/5.1.txt
Expand Up @@ -115,7 +115,10 @@ Minor features
:mod:`django.contrib.sessions`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* ...
* :class:`django.contrib.sessions.backends.cached_db.SessionStore` now handles
exceptions when storing session information in the cache, logging proper
error messages with their traceback via the newly added
:ref:`sessions logger <django-contrib-sessions-logger>`.

:mod:`django.contrib.sitemaps`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
14 changes: 11 additions & 3 deletions docs/topics/http/sessions.txt
Expand Up @@ -76,9 +76,17 @@ Once your cache is configured, you have to choose between a database-backed
cache or a non-persistent cache.

The cached database backend (``cached_db``) uses a write-through cache --
session writes are applied to both the cache and the database. Session reads
use the cache, or the database if the data has been evicted from the cache. To
use this backend, set :setting:`SESSION_ENGINE` to
session writes are applied to both the database and cache, in that order. If
writing to the cache fails, the exception is handled and logged via the
:ref:`sessions logger <django-contrib-sessions-logger>`, to avoid failing an
otherwise successful write operation.

.. versionchanged:: 5.1

Handling and logging of exceptions when writing to the cache was added.

Session reads use the cache, or the database if the data has been evicted from
the cache. To use this backend, set :setting:`SESSION_ENGINE` to
``"django.contrib.sessions.backends.cached_db"``, and follow the configuration
instructions for the `using database-backed sessions`_.

Expand Down
7 changes: 7 additions & 0 deletions tests/cache/failing_cache.py
@@ -0,0 +1,7 @@
from django.core.cache.backends.locmem import LocMemCache


class CacheClass(LocMemCache):

def set(self, *args, **kwargs):
raise Exception("Faked exception saving to cache")
16 changes: 16 additions & 0 deletions tests/sessions_tests/tests.py
Expand Up @@ -517,6 +517,22 @@ def test_non_default_cache(self):
with self.assertRaises(InvalidCacheBackendError):
self.backend()

@override_settings(
CACHES={"default": {"BACKEND": "cache.failing_cache.CacheClass"}}
)
def test_cache_set_failure_non_fatal(self):
"""Failing to write to the cache does not raise errors."""
session = self.backend()
session["key"] = "val"

with self.assertLogs("django.contrib.sessions", "ERROR") as cm:
session.save()

# A proper ERROR log message was recorded.
log = cm.records[-1]
self.assertEqual(log.message, f"Error saving to cache ({session._cache})")
self.assertEqual(str(log.exc_info[1]), "Faked exception saving to cache")


@override_settings(USE_TZ=True)
class CacheDBSessionWithTimeZoneTests(CacheDBSessionTests):
Expand Down