bookmark: add a test for a race condition on push stable
authorPierre-Yves David <pierre-yves.david@octobus.net>
Mon, 20 May 2019 10:06:53 +0200
branchstable
changeset 42322 d2c871b78c36
parent 42321 624080e8bd9f
child 42323 c2b83c957621
bookmark: add a test for a race condition on push Bookmark pointing to unknown nodes are ignored. Later these ignored bookmarks are dropped when writing the file back on disk. On paper, this behavior should be fine, but with the current implementation, it can lead to unexpected bookmark deletions. In theory, to make sure writer as a consistent view, taking the lock also invalidate bookmark data we already loaded into memory. However this invalidation is incomplete. The data are stored in a `filecache` that preserve them if the bookmark related file are untouched. In practice, the bookmark data in memory also depends of the changelog content, because of the step checking if the bookmarks refers to a node known to the changelog. So if the bookmark data were loaded from an up to date bookmark file but filtered with an outdated changelog file this go undetected. This condition is fairly specific, but can occurs very often in practice. We introduce a test recreating the situation. The test comes in an independant changeset to show it actually reproduce the situation. The fix will come soon after. A large share of the initial investigation of this race condition was made by Valentin Gatien-Baron <valentin.gatienbaron@gmail.com>.
tests/test-bookmarks-corner-case.t
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-bookmarks-corner-case.t	Mon May 20 10:06:53 2019 +0200
@@ -0,0 +1,238 @@
+================================
+Test corner case around bookmark
+================================
+
+This test file is meant to gather test around bookmark that are specific
+ enough to not find a place elsewhere.
+
+Test bookmark/changelog race condition
+======================================
+
+The data from the bookmark file are filtered to only contains bookmark with
+node known to the changelog. If the cache invalidation between these two bits
+goes wrong, bookmark can be dropped.
+
+global setup
+------------
+
+  $ cat >> $HGRCPATH << EOF
+  > [ui]
+  > ssh = "$PYTHON" "$TESTDIR/dummyssh"
+  > [server]
+  > concurrent-push-mode=check-related
+  > EOF
+
+Setup
+-----
+
+initial repository setup
+
+  $ hg init bookrace-server
+  $ cd bookrace-server
+  $ echo a > a
+  $ hg add a
+  $ hg commit -m root
+  $ echo a >> a
+  $ hg bookmark book-A
+  $ hg commit -m A0
+  $ hg up 'desc(root)'
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (leaving bookmark book-A)
+  $ echo b > b
+  $ hg add b
+  $ hg bookmark book-B
+  $ hg commit -m B0
+  created new head
+  $ hg up null
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  (leaving bookmark book-B)
+  $ hg phase --public --rev 'all()'
+  $ hg log -G
+  o  changeset:   2:c79985706978
+  |  bookmark:    book-B
+  |  tag:         tip
+  |  parent:      0:6569b5a81c7e
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B0
+  |
+  | o  changeset:   1:39c28d785860
+  |/   bookmark:    book-A
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     A0
+  |
+  o  changeset:   0:6569b5a81c7e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     root
+  
+  $ hg book
+     book-A                    1:39c28d785860
+     book-B                    2:c79985706978
+  $ cd ..
+
+Add new changeset on each bookmark in distinct clones
+
+  $ hg clone ssh://user@dummy/bookrace-server client-A
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 2 files (+1 heads)
+  new changesets 6569b5a81c7e:c79985706978
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg -R client-A update book-A
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  (activating bookmark book-A)
+  $ echo a >> client-A/a
+  $ hg -R client-A commit -m A1
+  $ hg clone ssh://user@dummy/bookrace-server client-B
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 2 files (+1 heads)
+  new changesets 6569b5a81c7e:c79985706978
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg -R client-B update book-B
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (activating bookmark book-B)
+  $ echo b >> client-B/b
+  $ hg -R client-B commit -m B1
+
+extension to reproduce the race
+-------------------------------
+
+If two process are pushing we want to make sure the following happens:
+
+* process A read changelog
+* process B to its full push
+* process A read bookmarks
+* process A proceed with rest of the push
+
+We build a server side extension for this purpose
+
+  $ cat > bookrace.py << EOF
+  > import os
+  > import time
+  > import atexit
+  > from mercurial import error, extensions, bookmarks
+  > def wrapinit(orig, self, repo):
+  >     if not os.path.exists('push-A-started'):
+  >         print('setting raced push up')
+  >         with open('push-A-started', 'w'):
+  >             pass
+  >     clock = 300
+  >     while not os.path.exists('push-B-done'):
+  >         clock -= 1
+  >         if clock <= 0:
+  >             raise error.Abort("race scenario timed out")
+  >         time.sleep(0.1)
+  >     return orig(self, repo)
+  > 
+  >     repo.__class__ = racedrepo
+  > def uisetup(ui):
+  >     extensions.wrapfunction(bookmarks.bmstore, '__init__', wrapinit)
+  > def e():
+  >     with open('push-A-done', 'w'):
+  >         pass
+  > atexit.register(e)
+  > EOF
+
+Actual test
+-----------
+
+Start the raced push.
+
+  $ cat >> bookrace-server/.hg/hgrc << EOF
+  > [extensions]
+  > bookrace=$TESTTMP/bookrace.py
+  > EOF
+  $ hg push -R client-A -r book-A >push-output.txt 2>&1 &
+
+Wait up to 30 seconds for that push to start.
+
+  $ clock=30
+  $ while [ ! -f push-A-started ] && [ $clock -gt 0 ] ; do
+  >    clock=`expr $clock - 1`
+  >    sleep 1
+  > done
+
+Do the other push.
+
+  $ cat >> bookrace-server/.hg/hgrc << EOF
+  > [extensions]
+  > bookrace=!
+  > EOF
+
+  $ hg push -R client-B -r book-B
+  pushing to ssh://user@dummy/bookrace-server
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  updating bookmark book-B
+
+Signal the raced put that we are done (it waits up to 30 seconds).
+
+  $ touch push-B-done
+
+Wait for the raced push to finish (with the remaning of the initial 30 seconds).
+
+  $ while [ ! -f push-A-done ] && [ $clock -gt 0 ] ; do
+  >    clock=`expr $clock - 1`
+  >    sleep 1
+  > done
+
+Check raced push output.
+
+  $ cat push-output.txt
+  pushing to ssh://user@dummy/bookrace-server
+  searching for changes
+  remote: setting raced push up
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  updating bookmark book-A
+
+Check result of the push.
+
+  $ hg -R bookrace-server log -G
+  o  changeset:   4:9ce3b28c16de
+  |  bookmark:    book-A
+  |  tag:         tip
+  |  parent:      1:39c28d785860
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A1
+  |
+  | o  changeset:   3:f26c3b5167d1
+  | |  bookmark:    book-B (false !)
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     B1
+  | |
+  | o  changeset:   2:c79985706978
+  | |  parent:      0:6569b5a81c7e
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     B0
+  | |
+  o |  changeset:   1:39c28d785860
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     A0
+  |
+  o  changeset:   0:6569b5a81c7e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     root
+  
+  $ hg -R bookrace-server book
+     book-A                    4:9ce3b28c16de
+     book-B                    3:f26c3b5167d1 (false !)