# HG changeset patch # User FUJIWARA Katsunori # Date 2016-05-18 15:20:38 # Node ID a109bf7e0dc2b2d4eefa6b6fd196494fbd0cac74 # Parent ca4065028e00160bbabd17016d8fde50eb561f55 util: make atomictempfile avoid ambiguity of file stat if needed Ambiguity check is executed at close(), only if: - atomictempfile is created with checkambig=True, and - target file exists before renaming This restriction avoids performance decrement by needless examination of file stat (for example, filelog doesn't need exact cache validation, even though it uses atomictempfile to write changes out). See description of filestat class for detail about why the logic in this patch works as expected. This patch is a part of preparation for "Exact Cache Validation Plan": https://www.mercurial-scm.org/wiki/ExactCacheValidationPlan diff --git a/mercurial/util.py b/mercurial/util.py --- a/mercurial/util.py +++ b/mercurial/util.py @@ -1453,11 +1453,12 @@ class atomictempfile(object): visible. If the object is destroyed without being closed, all your writes are discarded. ''' - def __init__(self, name, mode='w+b', createmode=None): + def __init__(self, name, mode='w+b', createmode=None, checkambig=False): self.__name = name # permanent name self._tempname = mktempcopy(name, emptyok=('w' in mode), createmode=createmode) self._fp = posixfile(self._tempname, mode) + self._checkambig = checkambig # delegated methods self.write = self._fp.write @@ -1468,7 +1469,17 @@ class atomictempfile(object): def close(self): if not self._fp.closed: self._fp.close() - rename(self._tempname, localpath(self.__name)) + filename = localpath(self.__name) + oldstat = self._checkambig and filestat(filename) + if oldstat and oldstat.stat: + rename(self._tempname, filename) + newstat = filestat(filename) + if newstat.isambig(oldstat): + # stat of changed file is ambiguous to original one + advanced = (oldstat.stat.st_mtime + 1) & 0x7fffffff + os.utime(filename, (advanced, advanced)) + else: + rename(self._tempname, filename) def discard(self): if not self._fp.closed: diff --git a/tests/test-atomictempfile.py b/tests/test-atomictempfile.py --- a/tests/test-atomictempfile.py +++ b/tests/test-atomictempfile.py @@ -42,6 +42,46 @@ class testatomictempfile(unittest.TestCa def test3_oops(self): self.assertRaises(TypeError, atomictempfile) + # checkambig=True avoids ambiguity of timestamp + def test4_checkambig(self): + def atomicwrite(checkambig): + f = atomictempfile('foo', checkambig=checkambig) + f.write('FOO') + f.close() + + # try some times, because reproduction of ambiguity depends on + # "filesystem time" + for i in xrange(5): + atomicwrite(False) + oldstat = os.stat('foo') + if oldstat.st_ctime != oldstat.st_mtime: + # subsequent changing never causes ambiguity + continue + + repetition = 3 + + # repeat atomic write with checkambig=True, to examine + # whether st_mtime is advanced multiple times as expecetd + for j in xrange(repetition): + atomicwrite(True) + newstat = os.stat('foo') + if oldstat.st_ctime != newstat.st_ctime: + # timestamp ambiguity was naturally avoided while repetition + continue + + # st_mtime should be advanced "repetition" times, because + # all atomicwrite() occured at same time (in sec) + self.assertTrue(newstat.st_mtime == + ((oldstat.st_mtime + repetition) & 0x7fffffff)) + # no more examination is needed, if assumption above is true + break + else: + # This platform seems too slow to examine anti-ambiguity + # of file timestamp (or test happened to be executed at + # bad timing). Exit silently in this case, because running + # on other faster platforms can detect problems + pass + if __name__ == '__main__': import silenttestrunner silenttestrunner.main(__name__)