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