repoview: discard filtered changelog if index isn't shared with unfiltered
authorFUJIWARA Katsunori <foozy@lares.dti.ne.jp>
Wed, 24 Feb 2016 06:10:46 +0900
changeset 28265 332926212ef8
parent 28264 3682e201cce6
child 28266 de8b09482fb7
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.
mercurial/repoview.py
tests/test-commandserver.t
--- 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