Patchwork [04,of,21,V2] speedy: support for running server against arbitrary repo

login
register
mail settings
Submitter Tomasz Kleczek
Date Dec. 14, 2012, 2:52 a.m.
Message ID <81a3c4c0455396f66dc0.1355453536@dev408.prn1.facebook.com>
Download mbox | patch
Permalink /patch/86/
State Deferred, archived
Headers show

Comments

Tomasz Kleczek - Dec. 14, 2012, 2:52 a.m.
# HG changeset patch
# User Tomasz Kleczek <tkleczek at fb.com>
# Date 1355419570 28800
# Node ID 81a3c4c0455396f66dc0bf5651b079a375c003fe
# Parent  767d988bfa2dada2286008e279af3241b4f7cdd8
speedy: support for running server against arbitrary repo

This is a step in making server standalone.

Queries are handled by the client in these steps:
1. Communicate with the server repository to find out
   the list of local changs (changes that are in the local
   repository history but aren't present in the server history).
2. Split the query into two parts: one that is run against the common
   client/server history (and is forwarded to the server) and
   one against local changes (computed locally).
3. Send the server part of the query to the server and wait
   for the response.
4. Locally compute the result for the local changesets.
5. Combine the answers from steps 3 and 4 and return the
   result to the caller.

Since we expect number of local changes to be small, this
provides a great performance improvement provided that server can
efficiently handle its portion of the query.

This change implements the above protocol for the revset.author query.

In the further description I refer to the repository against which
the history server is run as `serverrepo`.

This change adds `speedy.serverrepo` config option which specify
url of the serverrepo. The default value is the current repository.
(This means that the list of local changes will be empty, and the
server response will become the final response)

Please note that history server and its repository (serverrepo)
have been separated
from the client's point of view. Client must be configured with
the correct serverrepo path and in the future another config option
will be added for the 'history server path'.

This is a result of the fact that proxying client-serverrepo
communication through history server would require substantial
amount of extra work.
It is therefore client's responsibility to set the `serverrepo`
config option to match actual serverrepo.

Please note that setting it to a wrong path might result in a query
giving the wrong answer (some relevant changes might be skipped)!

Suppose the following scenario: There are two repositories of the
same project: repomain and repospeedy. repomain is the current
version of the project, repospeedy has a slightly older version
(e.g. lags 1h behind the main repo). The client should set
speedy.serverrepo = path_to_repospeedy
Suppose he sets it to path_to_mainrepo by mistake. Suppose further that
he pulls the latest version from the mainrepo and then performs some
history query. Then he might get some of his changes not included
in the results. What would happen here is client, after communicating
with serverrepo which is set to mainrepo path, would find out that
there are no local changes. However, history server is running against
older repo version so some changes committed in the last hour might
not be included in the answer.

If for some reason a history query cannot be split into two parts
as in step 2, it is probably not a good candidate to be handled
by this extension.

Patch

diff --git a/hgext/speedy/client.py b/hgext/speedy/client.py
--- a/hgext/speedy/client.py
+++ b/hgext/speedy/client.py
@@ -5,6 +5,7 @@ 
 
 from mercurial import extensions, commands
 from mercurial import revset
+from mercurial import hg, discovery, util
 from mercurial import localrepo
 from mercurial.i18n import _
 import server
@@ -12,18 +13,61 @@ 
 def nodestorevs(repo, nodes):
     return [repo[n].rev() for n in nodes if repo.changelog.hasnode(n)]
 
+def revstonodes(repo, revs):
+    return [repo[r].node() for r in revs]
+
+def exactlocalnodes(repo, remotepeer):
+    """Returns a tuple describing local nodes.
+
+    The tuple is: (last common node, list of local changesets' node ids).
+    """
+    # Temporary setting quiet to get rid of 'search for changes message'
+    # string printed to output
+    oldquiet = repo.ui.quiet
+    repo.ui.quiet = True
+    try:
+        outgoing = discovery.findcommonoutgoing(repo, remotepeer, [])
+    finally:
+        repo.ui.quiet = oldquiet
+
+    localnodes = outgoing.missing
+
+    # Finding the last common node
+    revs = list(nodestorevs(repo, outgoing.commonheads))
+    if revs:
+        lcommonrev = max(revs)
+    else:
+        lcommonrev = nullrev
+    lcommonnode = repo[lcommonrev].node()
+
+    return lcommonnode, localnodes
+
+
 class metapeer(object):
     """Class that encapsulates communication details with the metadata server.
 
     Its responsibilities:
         - delegating the query to the server
         - translating node ids from server to revision numbers
+        - computing and caching a list of local revs
     """
 
-    def __init__(self, server, repo):
+    def __init__(self, server, repo, serverrepopath):
+        self.serverrepopath = serverrepopath
         self._server = server
         self._repo = repo
 
+    @util.propertycache
+    def _localrevs(self):
+        remotepeer = hg.peer(self._repo, {}, self.serverrepopath)
+        lastcommonnode, localnodes = exactlocalnodes(self._repo, remotepeer)
+        localrevs = nodestorevs(self._repo, localnodes)
+        return localrevs
+
+    def localrevs(self):
+        """Return a list of local revisions."""
+        return self._localrevs
+
     def author(self, x):
         resp = self._server.author(x)
         return nodestorevs(self._repo, resp)
@@ -38,16 +82,17 @@ 
     # We want to catch errors early and on client, if possible
     pat = revset.getstring(x, _("author requires a string"))
     revs = set(metapeer.author(pat))
-    return [r for r in subset if r in revs]
+    lrevsall = metapeer.localrevs()
+    revs.update(revset.author(repo, lrevsall, ('symbol', pat)))
+    return [ r for r in subset if r in revs ]
 
 def _speedysetup(ui, repo):
     """Initialize speedy client."""
-    # For now, local repo and server repo are the same. This is going
-    # to change soon
-    serverrepo = localrepo.localrepository(ui, path=repo.root)
+    serverrepopath = ui.config('speedy', 'serverrepo', repo.root)
+    serverrepo = localrepo.localrepository(ui, path=serverrepopath)
     mserver = server.makeserver(serverrepo)
 
-    mpeer = metapeer(mserver, repo)
+    mpeer = metapeer(mserver, repo, serverrepopath)
 
     def wrapwithpeer(fun, peer):
         def wrapper(*args, **kwargs):
diff --git a/tests/test-speedy.t b/tests/test-speedy.t
--- a/tests/test-speedy.t
+++ b/tests/test-speedy.t
@@ -7,10 +7,13 @@ 
   > speedy=
   > EOF_END
 
-Preparing local repo
+  $ hg init serverrepo
+  $ cd serverrepo
 
-  $ hg init localrepo
-  $ cd localrepo
+  $ cat >> $TESTTMP/serverrepo/.hg/hgrc <<EOF_END
+  > [speedy]
+  > server = True
+  > EOF_END
 
   $ mkdir d1
   $ echo chg0 > d1/chg0
@@ -33,7 +36,80 @@ 
   $ hg commit -Am chg5 -u testuser1 --date "10/20/2012"
   adding chg5.py
 
+  $ cd $TESTTMP
+  $ hg clone serverrepo localrepo
+  updating to branch default
+  6 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ 
+  $ cd $TESTTMP/serverrepo
+
+  $ echo chgx > d2/chgx
+  $ hg commit -Am chgx
+  adding d2/chgx
+
+  $ cd $TESTTMP/localrepo
+
+  $ echo chg > d2/chgpushed
+  $ hg commit -Am chgpushed
+  adding d2/chgpushed
+
+  $ hg push -f
+  pushing to $TESTTMP/serverrepo
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+
+  $ echo chg > d1/chgl6
+  $ hg commit -Am chgl6 -u testuser1
+  adding d1/chgl6
+
+  $ hg pull
+  pulling from $TESTTMP/serverrepo
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+
+  $ cd $TESTTMP/serverrepo
+
+  $ echo chg6 > d1/chg6
+  $ hg commit -Am chg6 -u testuser1
+  adding d1/chg6
+  $ echo chg7 > d2/chg7.py
+  $ hg commit -Am chg7 -u testuser2
+  adding d2/chg7.py
+
+  $ mkdir d3_s
+  $ echo chg7_2 > d3_s/chg7_2.py
+  $ hg commit -Am chg7_1 -u testuser2
+  adding d3_s/chg7_2.py
+
+  $ cd $TESTTMP/localrepo
+
+  $ echo chg8 > d1/chg8
+  $ hg commit -Am chg8 -u testuser1
+  adding d1/chg8
+  $ echo chg9 > d2/chg9.py
+  $ hg commit -Am chg9 -u testuser2
+  adding d2/chg9.py
+  $ mkdir d3
+  $ echo chg10 > d3/chg10
+  $ hg commit -Am "chg10"
+  adding d3/chg10
+
+Writing local config file
+  $ cat >> $TESTTMP/localrepo/.hg/hgrc <<EOF_END
+  > [speedy]
+  > serverrepo = $TESTTMP/serverrepo
+  > EOF_END
+
   $ hg log -r "reverse(user(testuser1))"
+  chg8
+  chgl6
   chg5
   chg4
   chg3
@@ -42,3 +118,19 @@ 
 
   $ hg log -r "author(2)"
   chg1
+  chg9
+
+  $ hg log -r "reverse(author(test))"
+  chg10
+  chg9
+  chg8
+  chgx
+  chgl6
+  chgpushed
+  chg5
+  chg4
+  chg3
+  chg2
+  chg1
+  chg0
+