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
--- a/mercurial/util.py Thu May 19 00:20:37 2016 +0900
+++ b/mercurial/util.py Thu May 19 00:20:38 2016 +0900
@@ -1453,11 +1453,12 @@
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 @@
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:
--- a/tests/test-atomictempfile.py Thu May 19 00:20:37 2016 +0900
+++ b/tests/test-atomictempfile.py Thu May 19 00:20:38 2016 +0900
@@ -42,6 +42,46 @@
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__)