Repository: Kinto/kinto

Issue:
IntegrityError with Posgresql cache backend
I would like to update the current request with one of http://stackoverflow.com/a/1109198/186202


Discussion:

Resolving patch:
diff --git a/kinto/core/cache/postgresql/__init__.py b/kinto/core/cache/postgresql/__init__.py
--- a/kinto/core/cache/postgresql/__init__.py
+++ b/kinto/core/cache/postgresql/__init__.py
@@ -1,36 +1,13 @@
 from __future__ import absolute_import
-from functools import wraps
 
 import os
-import time
 
 from kinto.core import logger
 from kinto.core.cache import CacheBase
 from kinto.core.storage.postgresql.client import create_from_config
-from kinto.core.storage.exceptions import BackendError
 from kinto.core.utils import json
 
 
-DELAY_BETWEEN_RETRIES_IN_SECONDS = 0.005
-MAX_RETRIES = 10
-
-
-def retry_on_failure(func):
-    @wraps(func)
-    def wraps_func(self, *args, **kwargs):
-        tries = kwargs.pop('tries', 0)
-        try:
-            return func(self, *args, **kwargs)
-        except BackendError as e:
-            if tries < MAX_RETRIES:
-                # Skip delay the 2 first times.
-                delay = max(0, tries - 1) * DELAY_BETWEEN_RETRIES_IN_SECONDS
-                time.sleep(delay)
-                return wraps_func(self, tries=(tries + 1), *args, **kwargs)
-            raise e
-    return wraps_func
-
-
 class Cache(CacheBase):
     """Cache backend using PostgreSQL.
 
@@ -144,18 +121,15 @@ def expire(self, key, ttl):
         with self.client.connect() as conn:
             conn.execute(query, dict(ttl=ttl, key=self.prefix + key))
 
-    @retry_on_failure
     def set(self, key, value, ttl=None):
         if ttl is None:
             logger.warning("No TTL for cache key %r" % key)
         query = """
-        WITH upsert AS (
-            UPDATE cache SET value = :value, ttl = sec2ttl(:ttl)
-             WHERE key=:key
-            RETURNING *)
         INSERT INTO cache (key, value, ttl)
-        SELECT :key, :value, sec2ttl(:ttl)
-        WHERE NOT EXISTS (SELECT * FROM upsert)
+        VALUES (:key, :value, sec2ttl(:ttl))
+        ON CONFLICT (key) DO UPDATE
+        SET value = :value,
+            ttl = sec2ttl(:ttl);
         """
         value = json.dumps(value)
         with self.client.connect() as conn:


Test patch:
diff --git a/tests/core/test_cache.py b/tests/core/test_cache.py
--- a/tests/core/test_cache.py
+++ b/tests/core/test_cache.py
@@ -6,7 +6,6 @@
 from kinto.core.cache import (CacheBase, memory as memory_backend,
                               postgresql as postgresql_backend)
 from kinto.core.cache.testing import CacheTest
-from kinto.core.storage.exceptions import BackendError
 from kinto.core.testing import skip_if_no_postgresql
 
 
@@ -94,17 +93,3 @@ def setUp(self):
             self.cache.client,
             'session_factory',
             side_effect=sqlalchemy.exc.SQLAlchemyError)
-
-    def test_retry_on_integrity_error(self):
-        connect = mock.MagicMock()
-        connect.__enter__.return_value.execute.side_effect = [BackendError, None]
-        with mock.patch.object(self.cache.client, 'connect', return_value=connect):
-            self.cache.set('this[]', 'value')
-            assert connect.__enter__.return_value.execute.call_count == 2
-
-    def test_retry_on_integrity_error_and_raises_after_a_few_tries(self):
-        connect = mock.MagicMock()
-        connect.__enter__.return_value.execute.side_effect = BackendError
-        with mock.patch.object(self.cache.client, 'connect', return_value=connect):
-            with self.assertRaises(BackendError):
-                self.cache.set('this[]', 'value')