repoview: discard filtered changelog if index isn't shared with unfiltered
Before this patch, revisions rollbacked at failure of previous
transaction might be visible at subsequent operations unintentionally,
if repoview object is reused even after failure of transaction:
e.g. command server and HTTP server are typical cases.
'repoview' uses the tuple of values below of unfiltered changelog as
"the key" to examine validity of filtered changelog cache.
- length
- tip node
- filtered revisions (as hashed value)
- '_delayed' field
'repoview' compares between "the key" of unfiltered changelog at
previous caching and now, and reuses filtered changelog cache if no
change is detected.
But this comparison indicates only that there is no change between
unfiltered 'repo.changelog' at last caching and now, but not that
filtered changelog cache is valid for current unfiltered one.
'repoview' uses "shallow copy" of unfiltered changelog to create
filtered changelog cache. In this case, 'index' buffer of unfiltered
changelog is also referred by filtered changelog.
At failure of transaction, unfiltered changelog itself is invalidated
(= un-referred) on the 'repo' side (see 0a7610758c42 also). But
'index' of it still contains revisions to be rollbacked at this
failure, and is referred by filtered changelog.
Therefore, even if there is no change between unfiltered
'repo.changelog' at last caching and now, steps below makes rollbacked
revisions visible via filtered changelog unintentionally.
1. instantiate unfiltered changelog as 'repo.changelog'
(call it CL1)
2. make filtered (= shallow copy of) CL1
(call it FCL1)
3. cache FCL1 with "the key" of CL1
4. revisions are appended to 'index', which is shared by CL1 and FCL1
5. invalidate 'repo.changelog' (= CL1) at failure of transaction
6. instantiate 'repo.changelog' again at next operation
(call it CL2)
CL2 doesn't have revisions added at (4), because it is
instantiated from '00changelog.i', which isn't changed while
failed transaction.
7. compare between "the key" of CL1 and CL2
8. FCL1 cached at (3) is reused, because comparison at (7) doesn't
detect change between CL1 at (1) and CL2
9. revisions rollbacked at (5) are visible via FCL1 unintentionally,
because FCL1 still refers 'index' changed at (4)
The root cause of this issue is that there is no examination about
validity of filtered changelog cache against current unfiltered one.
This patch discards filtered changelog cache, if its 'index' object
isn't shared with unfiltered one.
BTW, at the time of this patch, redundant truncation of
'00changelog.i' at failure of transaction (see 0a7610758c42 for
detail) often prevents "hg serve" from making already rollbacked
revisions visible, because updating timestamps of '00changelog.i' by
truncation makes "hg serve" discard old repoview object with invalid
filtered changelog cache.
This is reason why this issue is overlooked before this patch, even
though test-bundle2-exchange.t has tests in similar situation: failure
of "hg push" via HTTP by pretxnclose hook on server side doesn't
prevent subsequent commands from looking up outgoing revisions
correctly.
But timestamp on the filesystem doesn't have enough resolution for
recent computation power, and it can't be assumed that this avoidance
always works as expected.
Therefore, without this patch, this issue might appear occasionally.
--- a/mercurial/repoview.py Fri Feb 26 15:22:46 2016 +0000
+++ b/mercurial/repoview.py Wed Feb 24 06:10:46 2016 +0900
@@ -315,7 +315,10 @@
revs = filterrevs(unfi, self.filtername)
cl = self._clcache
newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
- if cl is not None and newkey != self._clcachekey:
+ # if cl.index is not unfiindex, unfi.changelog would be
+ # recreated, and our clcache refers to garbage object
+ if (cl is not None and
+ (cl.index is not unfiindex or newkey != self._clcachekey)):
cl = None
# could have been made None by the previous if
if cl is None:
--- a/tests/test-commandserver.t Fri Feb 26 15:22:46 2016 +0000
+++ b/tests/test-commandserver.t Wed Feb 24 06:10:46 2016 +0900
@@ -725,3 +725,133 @@
[255]
#endif
+
+ $ cd ..
+
+Test that accessing to invalid changelog cache is avoided at
+subsequent operations even if repo object is reused even after failure
+of transaction (see 0a7610758c42 also)
+
+"hg log" after failure of transaction is needed to detect invalid
+cache in repoview: this can't detect by "hg verify" only.
+
+Combination of "finalization" and "empty-ness of changelog" (2 x 2 =
+4) are tested, because '00changelog.i' are differently changed in each
+cases.
+
+ $ cat > $TESTTMP/failafterfinalize.py <<EOF
+ > # extension to abort transaction after finalization forcibly
+ > from mercurial import commands, error, extensions, lock as lockmod
+ > def fail(tr):
+ > raise error.Abort('fail after finalization')
+ > def reposetup(ui, repo):
+ > class failrepo(repo.__class__):
+ > def commitctx(self, ctx, error=False):
+ > if self.ui.configbool('failafterfinalize', 'fail'):
+ > # 'sorted()' by ASCII code on category names causes
+ > # invoking 'fail' after finalization of changelog
+ > # using "'cl-%i' % id(self)" as category name
+ > self.currenttransaction().addfinalize('zzzzzzzz', fail)
+ > return super(failrepo, self).commitctx(ctx, error)
+ > repo.__class__ = failrepo
+ > EOF
+
+ $ hg init repo3
+ $ cd repo3
+
+ $ cat <<EOF >> $HGRCPATH
+ > [ui]
+ > logtemplate = {rev} {desc|firstline} ({files})\n
+ >
+ > [extensions]
+ > failafterfinalize = $TESTTMP/failafterfinalize.py
+ > EOF
+
+- test failure with "empty changelog"
+
+ $ echo foo > foo
+ $ hg add foo
+
+(failuer before finalization)
+
+ >>> from hgclient import readchannel, runcommand, check
+ >>> @check
+ ... def abort(server):
+ ... readchannel(server)
+ ... runcommand(server, ['commit',
+ ... '--config', 'hooks.pretxncommit=false',
+ ... '-mfoo'])
+ ... runcommand(server, ['log'])
+ ... runcommand(server, ['verify', '-q'])
+ *** runcommand commit --config hooks.pretxncommit=false -mfoo
+ transaction abort!
+ rollback completed
+ abort: pretxncommit hook exited with status 1
+ [255]
+ *** runcommand log
+ *** runcommand verify -q
+
+(failuer after finalization)
+
+ >>> from hgclient import readchannel, runcommand, check
+ >>> @check
+ ... def abort(server):
+ ... readchannel(server)
+ ... runcommand(server, ['commit',
+ ... '--config', 'failafterfinalize.fail=true',
+ ... '-mfoo'])
+ ... runcommand(server, ['log'])
+ ... runcommand(server, ['verify', '-q'])
+ *** runcommand commit --config failafterfinalize.fail=true -mfoo
+ transaction abort!
+ rollback completed
+ abort: fail after finalization
+ [255]
+ *** runcommand log
+ *** runcommand verify -q
+
+- test failure with "not-empty changelog"
+
+ $ echo bar > bar
+ $ hg add bar
+ $ hg commit -mbar bar
+
+(failure before finalization)
+
+ >>> from hgclient import readchannel, runcommand, check
+ >>> @check
+ ... def abort(server):
+ ... readchannel(server)
+ ... runcommand(server, ['commit',
+ ... '--config', 'hooks.pretxncommit=false',
+ ... '-mfoo', 'foo'])
+ ... runcommand(server, ['log'])
+ ... runcommand(server, ['verify', '-q'])
+ *** runcommand commit --config hooks.pretxncommit=false -mfoo foo
+ transaction abort!
+ rollback completed
+ abort: pretxncommit hook exited with status 1
+ [255]
+ *** runcommand log
+ 0 bar (bar)
+ *** runcommand verify -q
+
+(failure after finalization)
+
+ >>> from hgclient import readchannel, runcommand, check
+ >>> @check
+ ... def abort(server):
+ ... readchannel(server)
+ ... runcommand(server, ['commit',
+ ... '--config', 'failafterfinalize.fail=true',
+ ... '-mfoo', 'foo'])
+ ... runcommand(server, ['log'])
+ ... runcommand(server, ['verify', '-q'])
+ *** runcommand commit --config failafterfinalize.fail=true -mfoo foo
+ transaction abort!
+ rollback completed
+ abort: fail after finalization
+ [255]
+ *** runcommand log
+ 0 bar (bar)
+ *** runcommand verify -q