# HG changeset patch # User Goffi # Date 1521216226 -3600 # Node ID 35d591086974ce51406ec2c1e250d27989c3e618 # Parent e8e1507049b7ed1214a9d41d266706e772072172 core (memory, sqlite): added fileUpdate method to update "extra" and "access" avoiding race condition diff -r e8e1507049b7 -r 35d591086974 src/memory/memory.py --- 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): diff -r e8e1507049b7 -r 35d591086974 src/memory/sqlite.py --- 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):