瀏覽代碼

Fixed #34806 -- Made cached_db session backend resilient to cache write errors.

Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
Sulabh Katila 1 年之前
父節點
當前提交
eceb5e2eea

+ 8 - 1
django/contrib/sessions/backends/cached_db.py

@@ -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):
     """
@@ -52,7 +56,10 @@ class SessionStore(DBStore):
 
     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)

+ 11 - 0
docs/ref/logging.txt

@@ -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
 --------
 

+ 4 - 1
docs/releases/5.1.txt

@@ -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`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 11 - 3
docs/topics/http/sessions.txt

@@ -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`_.
 

+ 7 - 0
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 - 0
tests/sessions_tests/tests.py

@@ -517,6 +517,22 @@ class CacheDBSessionTests(SessionTestsMixin, TestCase):
         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):