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>.
--- /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 !)