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