@@ -23,19 +23,18 @@
$ hg commit -m_
transaction abort!
- rollback failed - please run hg recover
- (failure reason: attempted to truncate data/file.i to 1065 bytes, but it was already 128 bytes)
+ rollback completed
abort: pretxncommit hook exited with status 1
[40]
- $ cat .hg/store/journal | tr -s '\000' ' '
- data/file.i 1065
- data/file.d 0
- data/file.i 64
- 00manifest.i 111
- 00changelog.i 122
+Verify that rollback worked properly (the fncache being stale is a
+bug):
- $ hg recover
- rolling back interrupted transaction
- abort: attempted to truncate data/file.i to 1065 bytes, but it was already 128 bytes
- [255]
+ $ hg verify -q
+ warning: revlog 'data/file.d' not in fncache!
+ 1 warnings encountered!
+ hint: run "hg debugrebuildfncache" to recover from corrupt fncache
+ $ hg debugrebuildfncache
+ adding data/file.d
+ 1 items added, 0 removed from fncache
+ $ hg verify -q
@@ -46,6 +46,26 @@
return _active
+def _entries_to_playback(entries):
+ # On revlog split, we end up with two journal entries for the .i
+ # and .d. In these cases we want the second entries to override
+ # the first ones. This filters out the first entries.
+ usable_entries = []
+ seen_f = {}
+ for f, o in reversed(entries):
+ if f in seen_f:
+ if seen_f[f] != 1:
+ raise error.Abort(
+ _(b"unexpectedly many rollback journal entries for %s") % f
+ )
+ seen_f[f] = 2
+ else:
+ usable_entries.append((f, o))
+ seen_f[f] = 1
+ usable_entries.reverse()
+ return usable_entries
+
+
def _playback(
journal,
report,
@@ -56,7 +76,7 @@
unlink=True,
checkambigfiles=None,
):
- for f, o in entries:
+ for f, o in _entries_to_playback(entries):
if o or not unlink:
checkambig = checkambigfiles and (f, b'') in checkambigfiles
try:
@@ -1973,7 +1973,7 @@
_(b"%s not found in the transaction") % self._indexfile
)
trindex = 0
- tr.add(self._datafile, 0)
+ trdataoffset = 0
if fp:
fp.flush()
@@ -1983,10 +1983,22 @@
self._writinghandles = None
with self._indexfp(b'r') as ifh, self._datafp(b'w') as dfh:
+ if dfh.tell() != 0:
+ # Should never happen, but may reduce confusion if something
+ # went wrong in a rollback or manual fix.
+ raise error.RevlogError(
+ _(b"%s is an inline revlog but %s is nonempty")
+ % (self._indexfile, self._datafile)
+ )
+ # write this after the dfh.tell check, otherwise the rollback caused
+ # by a failure of the check would truncate whatever is the datafile
+ tr.add(self._datafile, 0)
for r in self:
dfh.write(self._getsegmentforrevs(r, r, df=ifh)[1])
if troffset <= self.start(r) + r * self.index.entry_size:
+ # trindex is the first revision to be excluded on rollback
trindex = r
+ trdataoffset = self.start(r)
with self._indexfp(b'w') as fp:
self._format_flags &= ~FLAG_INLINE_DATA
@@ -1999,9 +2011,24 @@
e = header + e
fp.write(e)
- # the temp file replace the real index when we exit the context
- # manager
-
+ # The temp file replace the real index when we exit the context
+ # manager. When that happens, transaction rollback is temporarily
+ # broken, because the rollback journal entry for the old .i is
+ # larger than the size of the new .i (because they include the same
+ # revisions, but offset for the old .i accounts for the inline
+ # data. Except when the data is of size 0, but that should be both
+ # impossible and harmless). This gets corrected very immediately by
+ # the tr.replace below. Note that we *must* write the offset for the
+ # (new) datafile before the offset for the new .i. In this order, if
+ # we get interrupted after the first tr.replace, the rollback
+ # fails. In the other order, if we got interrupted after the
+ # tr.replace for the index, rollback would succeed but leave the
+ # repository broken (non-inline index + datafile of size 0). One
+ # problem we still have is that since this change is done outside
+ # the transaction, the fncache should get updated even on
+ # transaction rollback, but doesn't.
+
+ tr.replace(self._datafile, trdataoffset)
tr.replace(self._indexfile, trindex * self.index.entry_size)
nodemaputil.setup_persistent_nodemap(tr, self)
self._chunkclear()