[web/views/staticcontrollers] Make ConcatFilesHandler write to a tempfile stable
authorJulien Cristau <julien.cristau@logilab.fr>
Thu, 11 Jul 2013 17:16:49 +0200
branchstable
changeset 9159 7a6ea3468765
parent 9158 08d8fba05854
child 9160 7db0c75acf1c
[web/views/staticcontrollers] Make ConcatFilesHandler write to a tempfile In order to be more resistant to errors such as ENOSPC, and against different threads accessing the same static data, we: - concatenate data files to a temporary file, and rename it once we're done - delete the tempfile in case of errors - protect the creation of the cache_concat file with a lock Closes #3005547
web/views/staticcontrollers.py
--- a/web/views/staticcontrollers.py	Fri Jul 12 18:03:11 2013 +0200
+++ b/web/views/staticcontrollers.py	Thu Jul 11 17:16:49 2013 +0200
@@ -27,6 +27,7 @@
 import os.path as osp
 import hashlib
 import mimetypes
+import threading
 from time import mktime
 from datetime import datetime, timedelta
 from logging import getLogger
@@ -105,6 +106,7 @@
         self._resources = {}
         self.config = config
         self.logger = getLogger('cubicweb.web')
+        self.lock = threading.Lock()
 
     def _resource(self, path):
         """get the resouce"""
@@ -143,21 +145,32 @@
     def concat_cached_filepath(self, paths):
         filepath = self.build_filepath(paths)
         if not self._up_to_date(filepath, paths):
-            with open(filepath, 'wb') as f:
-                for path in paths:
-                    dirpath, rid = self._resource(path)
-                    if rid is None:
-                        # In production mode log an error, do not return a 404
-                        # XXX the erroneous content is cached anyway
-                        self.logger.error('concatenated data url error: %r file '
-                                          'does not exist', path)
-                        if self.config.debugmode:
-                            raise NotFound(path)
-                    else:
-                        with open(osp.join(dirpath, rid), 'rb') as source:
-                            for line in source:
-                                f.write(line)
-                        f.write('\n')
+            tmpfile = filepath + '.tmp'
+            try:
+                with self.lock:
+                    if self._up_to_date(filepath, paths):
+                        # first check could have raced with some other thread
+                        # updating the file
+                        return filepath
+                    with open(tmpfile, 'wb') as f:
+                        for path in paths:
+                            dirpath, rid = self._resource(path)
+                            if rid is None:
+                                # In production mode log an error, do not return a 404
+                                # XXX the erroneous content is cached anyway
+                                self.logger.error('concatenated data url error: %r file '
+                                                  'does not exist', path)
+                                if self.config.debugmode:
+                                    raise NotFound(path)
+                            else:
+                                with open(osp.join(dirpath, rid), 'rb') as source:
+                                    for line in source:
+                                        f.write(line)
+                                f.write('\n')
+                    os.rename(tmpfile, filepath)
+            except:
+                os.remove(tmpfile)
+                raise
         return filepath