##// END OF EJS Templates
clonebundles: support for seeding clones from pre-generated bundles...
Gregory Szorc -
r26623:5a95fe44 default
parent child Browse files
Show More
@@ -0,0 +1,69
1 # This software may be used and distributed according to the terms of the
2 # GNU General Public License version 2 or any later version.
3
4 """server side extension to advertise pre-generated bundles to seed clones.
5
6 The extension essentially serves the content of a .hg/clonebundles.manifest
7 file to clients that request it.
8
9 The clonebundles.manifest file contains a list of URLs and attributes. URLs
10 hold pre-generated bundles that a client fetches and applies. After applying
11 the pre-generated bundle, the client will connect back to the original server
12 and pull data not in the pre-generated bundle.
13
14 Manifest File Format:
15
16 The manifest file contains a newline (\n) delimited list of entries.
17
18 Each line in this file defines an available bundle. Lines have the format:
19
20 <URL> [<key>=<value]
21
22 That is, a URL followed by extra metadata describing it. Metadata keys and
23 values should be URL encoded.
24
25 This metadata is optional. It is up to server operators to populate this
26 metadata.
27
28 Keys in UPPERCASE are reserved for use by Mercurial. All non-uppercase keys
29 can be used by site installations.
30
31 The server operator is responsible for generating the bundle manifest file.
32
33 Metadata Attributes:
34
35 TBD
36 """
37
38 from mercurial import (
39 extensions,
40 wireproto,
41 )
42
43 testedwith = 'internal'
44
45 def capabilities(orig, repo, proto):
46 caps = orig(repo, proto)
47
48 # Only advertise if a manifest exists. This does add some I/O to requests.
49 # But this should be cheaper than a wasted network round trip due to
50 # missing file.
51 if repo.opener.exists('clonebundles.manifest'):
52 caps.append('clonebundles')
53
54 return caps
55
56 @wireproto.wireprotocommand('clonebundles', '')
57 def bundles(repo, proto):
58 """Server command for returning info for available bundles to seed clones.
59
60 Clients will parse this response and determine what bundle to fetch.
61
62 Other extensions may wrap this command to filter or dynamically emit
63 data depending on the request. e.g. you could advertise URLs for
64 the closest data center given the client's IP address.
65 """
66 return repo.opener.tryread('clonebundles.manifest')
67
68 def extsetup(ui):
69 extensions.wrapfunction(wireproto, '_capabilities', capabilities)
@@ -0,0 +1,143
1 Set up a server
2
3 $ hg init server
4 $ cd server
5 $ cat >> .hg/hgrc << EOF
6 > [extensions]
7 > clonebundles =
8 > EOF
9
10 $ touch foo
11 $ hg -q commit -A -m 'add foo'
12 $ touch bar
13 $ hg -q commit -A -m 'add bar'
14
15 $ hg serve -d -p $HGPORT --pid-file hg.pid --accesslog access.log
16 $ cat hg.pid >> $DAEMON_PIDS
17 $ cd ..
18
19 Feature disabled by default
20 (client should not request manifest)
21
22 $ hg clone -U http://localhost:$HGPORT feature-disabled
23 requesting all changes
24 adding changesets
25 adding manifests
26 adding file changes
27 added 2 changesets with 2 changes to 2 files
28
29 $ cat server/access.log
30 * - - [*] "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
31 * - - [*] "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D (glob)
32 * - - [*] "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bundlecaps=HG20%2Cbundle2%3DHG20%250Achangegroup%253D01%252C02%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps&cg=1&common=0000000000000000000000000000000000000000&heads=aaff8d2ffbbf07a46dd1f05d8ae7877e3f56e2a2&listkeys=phase%2Cbookmarks (glob)
33 * - - [*] "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases (glob)
34
35 $ cat >> $HGRCPATH << EOF
36 > [experimental]
37 > clonebundles = true
38 > EOF
39
40 Missing manifest should not result in server lookup
41
42 $ hg --verbose clone -U http://localhost:$HGPORT no-manifest
43 requesting all changes
44 adding changesets
45 adding manifests
46 adding file changes
47 added 2 changesets with 2 changes to 2 files
48
49 $ tail -4 server/access.log
50 * - - [*] "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
51 * - - [*] "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D (glob)
52 * - - [*] "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bundlecaps=HG20%2Cbundle2%3DHG20%250Achangegroup%253D01%252C02%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps&cg=1&common=0000000000000000000000000000000000000000&heads=aaff8d2ffbbf07a46dd1f05d8ae7877e3f56e2a2&listkeys=phase%2Cbookmarks (glob)
53 * - - [*] "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases (glob)
54
55 Empty manifest file results in retrieval
56 (the extension only checks if the manifest file exists)
57
58 $ touch server/.hg/clonebundles.manifest
59 $ hg --verbose clone -U http://localhost:$HGPORT empty-manifest
60 no clone bundles available on remote; falling back to regular clone
61 requesting all changes
62 adding changesets
63 adding manifests
64 adding file changes
65 added 2 changesets with 2 changes to 2 files
66
67 Manifest file with invalid URL aborts
68
69 $ echo 'http://does.not.exist/bundle.hg' > server/.hg/clonebundles.manifest
70 $ hg clone http://localhost:$HGPORT 404-url
71 applying clone bundle from http://does.not.exist/bundle.hg
72 error fetching bundle: [Errno -2] Name or service not known
73 abort: error applying bundle
74 (consider contacting the server operator if this error persists)
75 [255]
76
77 Server is not running aborts
78
79 $ echo "http://localhost:$HGPORT1/bundle.hg" > server/.hg/clonebundles.manifest
80 $ hg clone http://localhost:$HGPORT server-not-runner
81 applying clone bundle from http://localhost:$HGPORT1/bundle.hg
82 error fetching bundle: [Errno 111] Connection refused
83 abort: error applying bundle
84 (consider contacting the server operator if this error persists)
85 [255]
86
87 Server returns 404
88
89 $ python $TESTDIR/dumbhttp.py -p $HGPORT1 --pid http.pid
90 $ cat http.pid >> $DAEMON_PIDS
91 $ hg clone http://localhost:$HGPORT running-404
92 applying clone bundle from http://localhost:$HGPORT1/bundle.hg
93 HTTP error fetching bundle: HTTP Error 404: File not found
94 abort: error applying bundle
95 (consider contacting the server operator if this error persists)
96 [255]
97
98 We can override failure to fall back to regular clone
99
100 $ hg --config ui.clonebundlefallback=true clone -U http://localhost:$HGPORT 404-fallback
101 applying clone bundle from http://localhost:$HGPORT1/bundle.hg
102 HTTP error fetching bundle: HTTP Error 404: File not found
103 falling back to normal clone
104 requesting all changes
105 adding changesets
106 adding manifests
107 adding file changes
108 added 2 changesets with 2 changes to 2 files
109
110 Bundle with partial content works
111
112 $ hg -R server bundle --type gzip --base null -r 53245c60e682 partial.hg
113 1 changesets found
114
115 $ echo "http://localhost:$HGPORT1/partial.hg" > server/.hg/clonebundles.manifest
116 $ hg clone -U http://localhost:$HGPORT partial-bundle
117 applying clone bundle from http://localhost:$HGPORT1/partial.hg
118 adding changesets
119 adding manifests
120 adding file changes
121 added 1 changesets with 1 changes to 1 files
122 finished applying clone bundle
123 searching for changes
124 adding changesets
125 adding manifests
126 adding file changes
127 added 1 changesets with 1 changes to 1 files
128
129 Bundle with full content works
130
131 $ hg -R server bundle --type gzip --base null -r tip full.hg
132 2 changesets found
133
134 $ echo "http://localhost:$HGPORT1/full.hg" > server/.hg/clonebundles.manifest
135 $ hg clone -U http://localhost:$HGPORT full-bundle
136 applying clone bundle from http://localhost:$HGPORT1/full.hg
137 adding changesets
138 adding manifests
139 adding file changes
140 added 2 changesets with 2 changes to 2 files
141 finished applying clone bundle
142 searching for changes
143 no changes found
@@ -7,12 +7,13
7 7
8 8 from i18n import _
9 9 from node import hex, nullid
10 import errno, urllib
10 import errno, urllib, urllib2
11 11 import util, scmutil, changegroup, base85, error
12 12 import discovery, phases, obsolete, bookmarks as bookmod, bundle2, pushkey
13 13 import lock as lockmod
14 14 import streamclone
15 15 import tags
16 import url as urlmod
16 17
17 18 def readbundle(ui, fh, fname, vfs=None):
18 19 header = changegroup.readexactly(fh, 4)
@@ -973,6 +974,9 def pull(repo, remote, heads=None, force
973 974 try:
974 975 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
975 976 streamclone.maybeperformlegacystreamclone(pullop)
977 # This should ideally be in _pullbundle2(). However, it needs to run
978 # before discovery to avoid extra work.
979 _maybeapplyclonebundle(pullop)
976 980 _pulldiscovery(pullop)
977 981 if pullop.canusebundle2:
978 982 _pullbundle2(pullop)
@@ -1499,3 +1503,88 def unbundle(repo, cg, heads, source, ur
1499 1503 if recordout is not None:
1500 1504 recordout(repo.ui.popbuffer())
1501 1505 return r
1506
1507 def _maybeapplyclonebundle(pullop):
1508 """Apply a clone bundle from a remote, if possible."""
1509
1510 repo = pullop.repo
1511 remote = pullop.remote
1512
1513 if not repo.ui.configbool('experimental', 'clonebundles', False):
1514 return
1515
1516 if pullop.heads:
1517 return
1518
1519 if not remote.capable('clonebundles'):
1520 return
1521
1522 res = remote._call('clonebundles')
1523 entries = parseclonebundlesmanifest(res)
1524
1525 # TODO filter entries by supported features.
1526 # TODO sort entries by user preferences.
1527
1528 if not entries:
1529 repo.ui.note(_('no clone bundles available on remote; '
1530 'falling back to regular clone\n'))
1531 return
1532
1533 url = entries[0]['URL']
1534 repo.ui.status(_('applying clone bundle from %s\n') % url)
1535 if trypullbundlefromurl(repo.ui, repo, url):
1536 repo.ui.status(_('finished applying clone bundle\n'))
1537 # Bundle failed.
1538 #
1539 # We abort by default to avoid the thundering herd of
1540 # clients flooding a server that was expecting expensive
1541 # clone load to be offloaded.
1542 elif repo.ui.configbool('ui', 'clonebundlefallback', False):
1543 repo.ui.warn(_('falling back to normal clone\n'))
1544 else:
1545 raise error.Abort(_('error applying bundle'),
1546 hint=_('consider contacting the server '
1547 'operator if this error persists'))
1548
1549 def parseclonebundlesmanifest(s):
1550 """Parses the raw text of a clone bundles manifest.
1551
1552 Returns a list of dicts. The dicts have a ``URL`` key corresponding
1553 to the URL and other keys are the attributes for the entry.
1554 """
1555 m = []
1556 for line in s.splitlines():
1557 fields = line.split()
1558 if not fields:
1559 continue
1560 attrs = {'URL': fields[0]}
1561 for rawattr in fields[1:]:
1562 key, value = rawattr.split('=', 1)
1563 attrs[urllib.unquote(key)] = urllib.unquote(value)
1564
1565 m.append(attrs)
1566
1567 return m
1568
1569 def trypullbundlefromurl(ui, repo, url):
1570 """Attempt to apply a bundle from a URL."""
1571 lock = repo.lock()
1572 try:
1573 tr = repo.transaction('bundleurl')
1574 try:
1575 try:
1576 fh = urlmod.open(ui, url)
1577 cg = readbundle(ui, fh, 'stream')
1578 changegroup.addchangegroup(repo, cg, 'clonebundles', url)
1579 tr.close()
1580 return True
1581 except urllib2.HTTPError as e:
1582 ui.warn(_('HTTP error fetching bundle: %s\n') % str(e))
1583 except urllib2.URLError as e:
1584 ui.warn(_('error fetching bundle: %s\n') % e.reason)
1585
1586 return False
1587 finally:
1588 tr.release()
1589 finally:
1590 lock.release()
@@ -1412,6 +1412,21 User interface controls.
1412 1412 default ``USER@HOST`` is used instead.
1413 1413 (default: False)
1414 1414
1415 ``clonebundlefallback``
1416 Whether failure to apply an advertised "clone bundle" from a server
1417 should result in fallback to a regular clone.
1418
1419 This is disabled by default because servers advertising "clone
1420 bundles" often do so to reduce server load. If advertised bundles
1421 start mass failing and clients automatically fall back to a regular
1422 clone, this would add significant and unexpected load to the server
1423 since the server is expecting clone operations to be offloaded to
1424 pre-generated bundles. Failing fast (the default behavior) ensures
1425 clients don't overwhelm the server when "clone bundle" application
1426 fails.
1427
1428 (default: False)
1429
1415 1430 ``commitsubrepos``
1416 1431 Whether to commit modified subrepositories when committing the
1417 1432 parent repository. If False and one subrepository has uncommitted
@@ -249,6 +249,8 Test extension help:
249 249 bugzilla hooks for integrating with the Bugzilla bug tracker
250 250 censor erase file content at a given revision
251 251 churn command to display statistics about repository history
252 clonebundles server side extension to advertise pre-generated bundles to
253 seed clones.
252 254 color colorize output from some commands
253 255 convert import revisions from foreign VCS repositories into
254 256 Mercurial
@@ -1069,6 +1071,8 Test keyword search help
1069 1071
1070 1072 Extensions:
1071 1073
1074 clonebundles server side extension to advertise pre-generated bundles to seed
1075 clones.
1072 1076 prefixedname matched against word "clone"
1073 1077 relink recreates hardlinks between repository clones
1074 1078
General Comments 0
You need to be logged in to leave comments. Login now