[PATCH 1 of 9 STABLE] commands: make commit acquire locks before processing (issue4368)

FUJIWARA Katsunori foozy at lares.dti.ne.jp
Tue Dec 1 18:18:39 UTC 2015


# HG changeset patch
# User FUJIWARA Katsunori <foozy at lares.dti.ne.jp>
# Date 1448993527 -32400
#      Wed Dec 02 03:12:07 2015 +0900
# Branch stable
# Node ID 2934ccaf3b6f3d35de6a9783a5cc2f7197af85c2
# Parent  6979fe2a6d75105affcacd9e298262a92641cb98
commands: make commit acquire locks before processing (issue4368)

Before this patch, "hg commit" (process A) executes steps below:

  1. get current branch heads via 'repo.branchheads()'
     - cache 'repo.changelog'
  2. invoke 'repo.commit()'
  3. acquire wlock
     - invalidate 'repo.dirstate'
  4. access 'repo.dirstate'
     - re-read '.hg/dirstate'
     - check validity of parent revisions with 'repo.changelog'
  5. invoke 'repo.commitctx()'
  6. acquire store lock (slock)
     - invalidate 'repo.changelog'
  7. do committing
  8. release slock
  9. release wlock
 10. check new branch head (via 'cmdutil.commitstatus()')

If acquisition of wlock at (3) above waits for another "hg commit"
(process B) or so running parallelly to release wlock, process A
causes creating orphan revision, because:

  - '.hg/dirstate' refers the revision, which is newly added by
    process B, as its parent

  - but already cached 'repo.changelog' doesn't contain such revision

  - therefore, validating parents of '.hg/dirstate' at (4) above
    replaces such revision with 'nullid'

Then, process A creates "orphan" revision, of which parent is "null"
revision.

In addition to it, "created new head" may be shown at the end of
process A unintentionally, if store is updated parallelly, because
both getting branch heads (1) and checking new branch head (10) are
executed outside slock scope.

To avoid this issue, this patch makes "hg commit" acquire wlock and
slock before processing.

This patch resolves the issue between "hg commit" processes, but not
one between "hg commit" and other commands. Subsequent patches resolve
the latter.

Even after this patch, there are still corner case problems below:

  - filecache may overlook changes of '.hg/dirstate', and it causes
    similar issue (see below for detail)

    https://bz.mercurial-scm.org/show_bug.cgi?id=4368#c10

  - 3rd party extension may cause similar issue, if it directly uses
    'repo.commit()' without acquisition of wlock and slock

    This can be fixed by acquisition of slock at the beginning of
    'repo.commit()', but it seems suitable for "default" branch

    In fact, acquisition of slock itself is already introduced at
    "default" branch by 4414d500604f, but acquisition is not at the
    beginning of 'repo.commit()'.

This patch also changes some tests:

  - test-fncache.t needs this tricky wrapping, to release (= forced
    failure of) wlock certainly

  - order of "hg commit" output is changed by widening scope of locks,
    because some hooks are fired after releasing wlock

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -1506,6 +1506,15 @@ def commit(ui, repo, *pats, **opts):
 
     Returns 0 on success, 1 if nothing changed.
     """
+    wlock = lock = None
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+        return _docommit(ui, repo, *pats, **opts)
+    finally:
+        release(lock, wlock)
+
+def _docommit(ui, repo, *pats, **opts):
     if opts.get('interactive'):
         opts.pop('interactive')
         cmdutil.dorecord(ui, repo, commit, None, False,
diff --git a/tests/test-fncache.t b/tests/test-fncache.t
--- a/tests/test-fncache.t
+++ b/tests/test-fncache.t
@@ -203,7 +203,7 @@ Aborting lock does not prevent fncache w
   $ cat > exceptionext.py <<EOF
   > import os
   > from mercurial import commands, error
-  > from mercurial.extensions import wrapfunction
+  > from mercurial.extensions import wrapcommand, wrapfunction
   > 
   > def lockexception(orig, vfs, lockname, wait, releasefn, *args, **kwargs):
   >     def releasewrap():
@@ -215,6 +215,22 @@ Aborting lock does not prevent fncache w
   > 
   > cmdtable = {}
   > 
+  > # wrap "commit" command to prevent wlock from being '__del__()'-ed
+  > # at the end of dispatching (for intentional "forced lcok failure")
+  > def commitwrap(orig, ui, repo, *pats, **opts):
+  >     repo = repo.unfiltered() # to use replaced repo._lock certainly
+  >     wlock = repo.wlock()
+  >     try:
+  >         return orig(ui, repo, *pats, **opts)
+  >     finally:
+  >         # multiple 'relase()' is needed for complete releasing wlock,
+  >         # because "forced" abort at last releasing store lock
+  >         # prevents wlock from being released at same 'lockmod.release()'
+  >         for i in range(wlock.held):
+  >             wlock.release()
+  > 
+  > def extsetup(ui):
+  >     wrapcommand(commands.table, "commit", commitwrap)
   > EOF
   $ extpath=`pwd`/exceptionext.py
   $ hg init fncachetxn
diff --git a/tests/test-hook.t b/tests/test-hook.t
--- a/tests/test-hook.t
+++ b/tests/test-hook.t
@@ -81,10 +81,10 @@ pretxncommit and commit hooks can see bo
   pretxncommit hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PENDING=$TESTTMP/a
   2:ee9deb46ab31
   pretxnclose hook: HG_PENDING=$TESTTMP/a HG_TXNID=TXN:* HG_TXNNAME=commit (glob)
+  created new head
   txnclose hook: HG_TXNID=TXN:* HG_TXNNAME=commit (glob)
   commit hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
   commit.b hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
-  created new head
   $ hg merge 1
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   (branch merge, don't forget to commit)
@@ -563,9 +563,9 @@ make sure --traceback works
   foo
   committing manifest
   committing changelog
+  committed changeset 1:52998019f6252a2b893452765fcb0a47351a5708
   calling hook commit.auto: hgext_hookext.autohook
   Automatically installed hook
-  committed changeset 1:52998019f6252a2b893452765fcb0a47351a5708
 
   $ hg showconfig hooks
   hooks.commit.auto=<function autohook at *> (glob)
diff --git a/tests/test-keyword.t b/tests/test-keyword.t
--- a/tests/test-keyword.t
+++ b/tests/test-keyword.t
@@ -141,8 +141,8 @@ Commit with several checks
   committing manifest
   committing changelog
   overwriting a expanding keywords
+  committed changeset 1:ef63ca68695bc9495032c6fda1350c71e6d256e9
   running hook commit.test: cp a hooktest
-  committed changeset 1:ef63ca68695bc9495032c6fda1350c71e6d256e9
   $ hg status
   ? hooktest
   $ hg debugrebuildstate


More information about the Mercurial-devel mailing list