changeset 2526:35d591086974

core (memory, sqlite): added fileUpdate method to update "extra" and "access" avoiding race condition
author Goffi <goffi@goffi.org>
date Fri, 16 Mar 2018 17:03:46 +0100
parents e8e1507049b7
children a201194fc461
files src/memory/memory.py src/memory/sqlite.py
diffstat 2 files changed, 54 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/src/memory/memory.py	Fri Mar 16 17:00:57 2018 +0100
+++ b/src/memory/memory.py	Fri Mar 16 17:03:46 2018 +0100
@@ -1302,6 +1302,18 @@
                                    owner=owner,
                                    access=access, extra=extra)
 
+    def fileUpdate(self, file_id, column, update_cb):
+        """update a file column taking care of race condition
+
+        access is NOT checked in this method, it must be checked beforehand
+        @param file_id(unicode): id of the file to update
+        @param column(unicode): one of "access" or "extra"
+        @param update_cb(callable): method to update the value of the colum
+            the method will take older value as argument, and must update it in place
+            Note that the callable must be thread-safe
+        """
+        return self.storage.fileUpdate(file_id, column, update_cb)
+
     ## Misc ##
 
     def isEntityAvailable(self, entity_jid, profile_key):
--- a/src/memory/sqlite.py	Fri Mar 16 17:00:57 2018 +0100
+++ b/src/memory/sqlite.py	Fri Mar 16 17:03:46 2018 +0100
@@ -798,6 +798,48 @@
         d.addErrback(lambda failure: log.error(_(u"Can't save file metadata for [{profile}]: {reason}".format(profile=client.profile, reason=failure))))
         return d
 
+    def _fileUpdate(self, cursor, file_id, column, update_cb):
+        query = 'SELECT {column} FROM files where id=?'.format(column=column)
+        for i in xrange(5):
+            cursor.execute(query, [file_id])
+            try:
+                older_value_raw = cursor.fetchone()[0]
+            except TypeError:
+                raise exceptions.NotFound
+            value = json.loads(older_value_raw)
+            update_cb(value)
+            value_raw = json.dumps(value)
+            update_query = 'UPDATE files SET {column}=? WHERE id=? AND {column}=?'.format(column=column)
+            update_args = (value_raw, file_id, older_value_raw)
+            try:
+                cursor.execute(update_query, update_args)
+            except sqlite3.Error:
+                pass
+            else:
+                if cursor.rowcount == 1:
+                    break;
+            log.warning(_(u"table not updated, probably due to race condition, trying again ({tries})").format(tries=i+1))
+        else:
+            log.error(_(u"Can't update file table"))
+
+    def fileUpdate(self, file_id, column, update_cb):
+        """update a column value using a method to avoid race conditions
+
+        the older value will be retrieved from database, then update_cb will be applied
+        to update it, and file will be updated checking that older value has not been changed meanwhile
+        by an other user. If it has changed, it tries again a couple of times before failing
+        @param column(str): column name (only "access" or "extra" are allowed)
+        @param update_cb(callable): method to update the value of the colum
+            the method will take older value as argument, and must update it in place
+            update_cb must not care about serialization,
+            it get the deserialized data (i.e. a Python object) directly
+            Note that the callable must be thread-safe
+        @raise exceptions.NotFound: there is not file with this id
+        """
+        if column not in ('access', 'extra'):
+            raise exceptions.InternalError('bad column name')
+        return self.dbpool.runInteraction(self._fileUpdate, file_id, column, update_cb)
+
     ##Helper methods##
 
     def __getFirstResult(self, result):