Patchwork [11,of,11] protocol: send application/mercurial-0.2 responses to capable clients

login
register
mail settings
Submitter Gregory Szorc
Date Nov. 20, 2016, 10:23 p.m.
Message ID <234bf232624bb6419859.1479680628@ubuntu-vm-main>
Download mbox | patch
Permalink /patch/17662/
State Accepted
Headers show

Comments

Gregory Szorc - Nov. 20, 2016, 10:23 p.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@gmail.com>
# Date 1479679822 28800
#      Sun Nov 20 14:10:22 2016 -0800
# Node ID 234bf232624bb6419859a64bb3ef977f92822343
# Parent  bae72dfc96ca2eb58c1484148a8585c6b25cc0b6
protocol: send application/mercurial-0.2 responses to capable clients

With this commit, the HTTP transport now parses the Accept header
to determine what media type and compression engine to use for
responses. So far, we only compress responses that are already being
compressed with zlib today (stream response types to specific
commands). We can expand things to cover additional response types
later.

As the inline comments explain, the parser for the Accept header
is far from robust. Honestly, I have reservations about this because
limitations in Accept header parsing will hinder us from expanding
it later. And, our server may not interop with clients that don't
format things just right.

There are a few alternatives to this:

1) Require the client to only send application/mercurial-0.2
   (this also limits us from expanding Accept in the future)
2) Use a non-standard, X-* request header to communicate compression
   support

The practical side-effect of this commit is that non-zlib compression
engines will be used if both ends support them. This means if both
ends have zstd support, zstd - not zlib - will be used to compress
data!

When cloning the mozilla-unified repository between a local HTTP
server and client, the benefits of non-zlib compression are quite
noticeable:

  engine     server CPU (s)    client CPU (s)
zlib (l=6)      185.4             283.2
zstd (l=1)       97.6             267.3
zstd (l=3)      103.6             266.9
zstd (l=7)      132.5             269.7
none             93.7             277.2

The default zstd compression level is 3. So if you deploy zstd
capable Mercurial to your clients and servers and CPU time on
your server is dominated by "getbundle" requests (clients cloning
and pulling) - and my experience at Mozilla tells me this is often
the case - this commit could nearly halve your server-side CPU usage!
And as the numbers in 41a8106789ca show, zstd also yields better
compression of bundles than zlib, so you are saving on bandwidth too.
zstd is wins all around!

Another benefit of this change is that server operators can install
*any* compression engine. While it isn't enabled by default, the
"none" compression engine can now be used to disable wire protocol
compression completely. Previously, commands like "getbundle" always
zlib compressed output, adding considerable overhead to generating
responses. If you are on a high speed network and your server is under
high load, it might be advantageous to trade bandwidth for CPU.
Although, zstd at level 1 doesn't use that much CPU, so I'm not
convinced that disabling compression wholesale is worthwhile. And, my
data seems to indicate a slow down on the client without compression.
I suspect this is due to a lack of buffering resulting in an increase
in socket read() calls and/or the fact we're transferring an extra 3 GB
of data (parsing HTTP chunked transfer and processing extra TCP packets
can add up). This is definitely worth investigating and optimizing. But
since the "none" compressor isn't enabled by default, I'm inclined to
punt on this issue.

This commit introduces tons of tests. Some of these should arguably
have been implemented on previous commits. But it was difficult to
test without the server functionality in place.
Augie Fackler - Nov. 21, 2016, 11:12 p.m.
On Sun, Nov 20, 2016 at 02:23:48PM -0800, Gregory Szorc wrote:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@gmail.com>
> # Date 1479679822 28800
> #      Sun Nov 20 14:10:22 2016 -0800
> # Node ID 234bf232624bb6419859a64bb3ef977f92822343
> # Parent  bae72dfc96ca2eb58c1484148a8585c6b25cc0b6
> protocol: send application/mercurial-0.2 responses to capable clients
>
> With this commit, the HTTP transport now parses the Accept header
> to determine what media type and compression engine to use for
> responses. So far, we only compress responses that are already being
> compressed with zlib today (stream response types to specific
> commands). We can expand things to cover additional response types
> later.
>
> As the inline comments explain, the parser for the Accept header
> is far from robust. Honestly, I have reservations about this because
> limitations in Accept header parsing will hinder us from expanding
> it later. And, our server may not interop with clients that don't
> format things just right.
>
> There are a few alternatives to this:
>
> 1) Require the client to only send application/mercurial-0.2
>    (this also limits us from expanding Accept in the future)
> 2) Use a non-standard, X-* request header to communicate compression
>    support
>
> The practical side-effect of this commit is that non-zlib compression
> engines will be used if both ends support them. This means if both
> ends have zstd support, zstd - not zlib - will be used to compress
> data!
>
> When cloning the mozilla-unified repository between a local HTTP
> server and client, the benefits of non-zlib compression are quite
> noticeable:
>
>   engine     server CPU (s)    client CPU (s)
> zlib (l=6)      185.4             283.2
> zstd (l=1)       97.6             267.3
> zstd (l=3)      103.6             266.9
> zstd (l=7)      132.5             269.7
> none             93.7             277.2

Can I trouble you to add "number of bytes transferred" as a column in
this table?

>
> The default zstd compression level is 3. So if you deploy zstd
> capable Mercurial to your clients and servers and CPU time on
> your server is dominated by "getbundle" requests (clients cloning
> and pulling) - and my experience at Mozilla tells me this is often
> the case - this commit could nearly halve your server-side CPU usage!
> And as the numbers in 41a8106789ca show, zstd also yields better
> compression of bundles than zlib, so you are saving on bandwidth too.
> zstd is wins all around!
>
> Another benefit of this change is that server operators can install
> *any* compression engine. While it isn't enabled by default, the
> "none" compression engine can now be used to disable wire protocol
> compression completely. Previously, commands like "getbundle" always
> zlib compressed output, adding considerable overhead to generating
> responses. If you are on a high speed network and your server is under
> high load, it might be advantageous to trade bandwidth for CPU.
> Although, zstd at level 1 doesn't use that much CPU, so I'm not
> convinced that disabling compression wholesale is worthwhile. And, my
> data seems to indicate a slow down on the client without compression.
> I suspect this is due to a lack of buffering resulting in an increase
> in socket read() calls and/or the fact we're transferring an extra 3 GB
> of data (parsing HTTP chunked transfer and processing extra TCP packets
> can add up). This is definitely worth investigating and optimizing. But
> since the "none" compressor isn't enabled by default, I'm inclined to
> punt on this issue.
>
> This commit introduces tons of tests. Some of these should arguably
> have been implemented on previous commits. But it was difficult to
> test without the server functionality in place.
>
> diff --git a/mercurial/hgweb/protocol.py b/mercurial/hgweb/protocol.py
> --- a/mercurial/hgweb/protocol.py
> +++ b/mercurial/hgweb/protocol.py
> @@ -8,6 +8,7 @@
>  from __future__ import absolute_import
>
>  import cgi
> +import re
>
>  from .common import (
>      HTTP_OK,
> @@ -23,6 +24,7 @@ urlerr = util.urlerr
>  urlreq = util.urlreq
>
>  HGTYPE = 'application/mercurial-0.1'
> +HGTYPE2 = 'application/mercurial-0.2'
>  HGERRTYPE = 'application/hg-error'
>
>  class webproto(wireproto.abstractserverproto):
> @@ -75,24 +77,88 @@ class webproto(wireproto.abstractserverp
>          self.ui.ferr, self.ui.fout = self.oldio
>          return val
>
> -    def compresschunks(self, chunks):
> -        # Don't allow untrusted settings because disabling compression or
> -        # setting a very high compression level could lead to flooding
> -        # the server's network or CPU.
> -        opts = {'level': self.ui.configint('server', 'zliblevel', -1)}
> -        return util.compengines['zlib'].compressstream(chunks, opts)
> -
>      def _client(self):
>          return 'remote:%s:%s:%s' % (
>              self.req.env.get('wsgi.url_scheme') or 'http',
>              urlreq.quote(self.req.env.get('REMOTE_HOST', '')),
>              urlreq.quote(self.req.env.get('REMOTE_USER', '')))
>
> +    def responsetype(self, v1compressible=False):
> +        """Determine the appropriate response type and compression settings.
> +
> +        The ``v1compressible`` argument states whether the response with
> +        application/mercurial-0.1 media types should be zlib compressed.
> +
> +        Returns a tuple of (mediatype, compengine, engineopts).
> +        """
> +        # For now, if it isn't compressible in the old world, it's never
> +        # compressible. We can change this to send uncompressed 0.2 payloads
> +        # later.
> +        if not v1compressible:
> +            return HGTYPE, None, None
> +
> +        # Determine the response media type and compression engine based
> +        # on the request parameters.
> +        accept = self.req.env.get('HTTP_ACCEPT', '')
> +
> +        if HGTYPE2 in accept:
> +            # This is far from a robust parser of the Accept header. A proper
> +            # implementation would require a lot of code. Until we actually
> +            # need it, cheat and assume that if the "0.2" media type is present
> +            # that it is preferred.

It seems robust enough for anything we'll see in the wild, including
if someone has a version 0.20 accept in here.

See below where I start waffling on the fallback in the presence of a
crazy accept header for more rumination.

> +            remaining = accept[accept.index(HGTYPE2) + len(HGTYPE2):]
> +            # We also cheat and assume commas cannot appear in parameters. In
> +            # reality, they can appear inside quoted strings. This means
> +            # a comma inside quotes and "application/mercurial-0.2" can never
> +            # appear in an Accept header without this code breaking. Hopefully
> +            # by the time this becomes a problem we have invented a new, better
> +            # wire protocol.
> +            m = re.match('^;\s*(?<!,)comp="([^"]+)"', remaining)
> +            if m:
> +                compformats = m.group(1).split()
> +            else:
> +                # Default as defined by wire protocol spec.
> +                compformats = ['ZL', 'UN']
> +
> +            # Now find an agreed upon compression format.
> +            for engine in wireproto.supportedcompengines(self.ui, self,
> +                                                         'server'):
> +                if engine.wireprotosupport()[0] in compformats:
> +                    opts = {}
> +                    level = self.ui.configint('server',
> +                                              '%slevel' % engine.name())
> +                    if level is not None:
> +                        opts['level'] = level
> +
> +                    return HGTYPE2, engine, opts
> +
> +            # No mutually supported compression format. Fall back to the
> +            # legacy protocol.
> +
> +        # Don't allow untrusted settings because disabling compression or
> +        # setting a very high compression level could lead to flooding
> +        # the server's network or CPU.
> +        opts = {'level': self.ui.configint('server', 'zliblevel', -1)}
> +        return HGTYPE, util.compengines['zlib'], opts
> +

[...]

> diff --git a/tests/test-http-protocol.t b/tests/test-http-protocol.t
> --- a/tests/test-http-protocol.t
> +++ b/tests/test-http-protocol.t
> @@ -43,3 +43,134 @@ Order of engines can also change
>    compression=UN,ZL
>
>    $ killdaemons.py
> +
> +Start a default server again
> +
> +  $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
> +  $ cat hg.pid > $DAEMON_PIDS
> +
> +Server should send application/mercurial-0.1 to clients if no Accept is used
> +
> +  $ get-with-headers.py --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
> +  200 Script output follows
> +  content-type: application/mercurial-0.1
> +  date: * (glob)
> +  server: * (glob)
> +  transfer-encoding: chunked
> +
> +Server should send application/mercurial-0.1 when client sends it
> +
> +  $ get-with-headers.py --accept 'application/mercurial-0.1' --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
> +  200 Script output follows
> +  content-type: application/mercurial-0.1
> +  date: * (glob)
> +  server: * (glob)
> +  transfer-encoding: chunked
> +
> +Server should send application/mercurial-0.2 when client sends it
> +
> +  $ get-with-headers.py --accept 'application/mercurial-0.2' --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
> +  200 Script output follows
> +  content-type: application/mercurial-0.2
> +  date: * (glob)
> +  server: * (glob)
> +  transfer-encoding: chunked
> +
> +  $ get-with-headers.py --accept 'application/mercurial-0.1; q=0.5, application/mercurial-0.2' --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
> +  200 Script output follows
> +  content-type: application/mercurial-0.2
> +  date: * (glob)
> +  server: * (glob)
> +  transfer-encoding: chunked
> +
> +Requesting a compression format that server doesn't support results will fall back to 0.1
> +
> +  $ get-with-headers.py --accept 'application/mercurial-0.2; comp="AA"' --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
> +  200 Script output follows
> +  content-type: application/mercurial-0.1
> +  date: * (glob)
> +  server: * (glob)
> +  transfer-encoding: chunked

I'm starting to waver on this. I'm wondering if a client is dumb
enough to say "I only want version two responses in my crazy
compression scheme" if we shouldn't respond "sorry, can't" so that the
failure mode for a broken config is noisy rather than silently doing
something slow. Thoughts?

> +
> +#if zstd
> +zstd is used if available
> +
> +  $ get-with-headers.py --accept 'application/mercurial-0.2; comp="ZS"' 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
> +  $ f --size --hexdump --bytes 33 --sha1 resp
> +  resp: size=245, sha1=821812c6bdb516252b2cefb76d2effd5e
> +  0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
> +  0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 5a 53 28 b5 2f |t follows..ZS(./|
> +  0020: fd                                              |.|
> +

Patch

diff --git a/mercurial/hgweb/protocol.py b/mercurial/hgweb/protocol.py
--- a/mercurial/hgweb/protocol.py
+++ b/mercurial/hgweb/protocol.py
@@ -8,6 +8,7 @@ 
 from __future__ import absolute_import
 
 import cgi
+import re
 
 from .common import (
     HTTP_OK,
@@ -23,6 +24,7 @@  urlerr = util.urlerr
 urlreq = util.urlreq
 
 HGTYPE = 'application/mercurial-0.1'
+HGTYPE2 = 'application/mercurial-0.2'
 HGERRTYPE = 'application/hg-error'
 
 class webproto(wireproto.abstractserverproto):
@@ -75,24 +77,88 @@  class webproto(wireproto.abstractserverp
         self.ui.ferr, self.ui.fout = self.oldio
         return val
 
-    def compresschunks(self, chunks):
-        # Don't allow untrusted settings because disabling compression or
-        # setting a very high compression level could lead to flooding
-        # the server's network or CPU.
-        opts = {'level': self.ui.configint('server', 'zliblevel', -1)}
-        return util.compengines['zlib'].compressstream(chunks, opts)
-
     def _client(self):
         return 'remote:%s:%s:%s' % (
             self.req.env.get('wsgi.url_scheme') or 'http',
             urlreq.quote(self.req.env.get('REMOTE_HOST', '')),
             urlreq.quote(self.req.env.get('REMOTE_USER', '')))
 
+    def responsetype(self, v1compressible=False):
+        """Determine the appropriate response type and compression settings.
+
+        The ``v1compressible`` argument states whether the response with
+        application/mercurial-0.1 media types should be zlib compressed.
+
+        Returns a tuple of (mediatype, compengine, engineopts).
+        """
+        # For now, if it isn't compressible in the old world, it's never
+        # compressible. We can change this to send uncompressed 0.2 payloads
+        # later.
+        if not v1compressible:
+            return HGTYPE, None, None
+
+        # Determine the response media type and compression engine based
+        # on the request parameters.
+        accept = self.req.env.get('HTTP_ACCEPT', '')
+
+        if HGTYPE2 in accept:
+            # This is far from a robust parser of the Accept header. A proper
+            # implementation would require a lot of code. Until we actually
+            # need it, cheat and assume that if the "0.2" media type is present
+            # that it is preferred.
+            remaining = accept[accept.index(HGTYPE2) + len(HGTYPE2):]
+            # We also cheat and assume commas cannot appear in parameters. In
+            # reality, they can appear inside quoted strings. This means
+            # a comma inside quotes and "application/mercurial-0.2" can never
+            # appear in an Accept header without this code breaking. Hopefully
+            # by the time this becomes a problem we have invented a new, better
+            # wire protocol.
+            m = re.match('^;\s*(?<!,)comp="([^"]+)"', remaining)
+            if m:
+                compformats = m.group(1).split()
+            else:
+                # Default as defined by wire protocol spec.
+                compformats = ['ZL', 'UN']
+
+            # Now find an agreed upon compression format.
+            for engine in wireproto.supportedcompengines(self.ui, self,
+                                                         'server'):
+                if engine.wireprotosupport()[0] in compformats:
+                    opts = {}
+                    level = self.ui.configint('server',
+                                              '%slevel' % engine.name())
+                    if level is not None:
+                        opts['level'] = level
+
+                    return HGTYPE2, engine, opts
+
+            # No mutually supported compression format. Fall back to the
+            # legacy protocol.
+
+        # Don't allow untrusted settings because disabling compression or
+        # setting a very high compression level could lead to flooding
+        # the server's network or CPU.
+        opts = {'level': self.ui.configint('server', 'zliblevel', -1)}
+        return HGTYPE, util.compengines['zlib'], opts
+
 def iscmd(cmd):
     return cmd in wireproto.commands
 
 def call(repo, req, cmd):
     p = webproto(req, repo.ui)
+
+    def genversion2(gen, compress, engine, engineopts):
+        # application/mercurial-0.2 always sends 2 byte payload header
+        # identifying compression engine.
+        yield engine.wireprotosupport()[0]
+
+        if compress:
+            for chunk in engine.compressstream(gen, opts=engineopts):
+                yield chunk
+        else:
+            for chunk in gen:
+                yield chunk
+
     rsp = wireproto.dispatch(repo, p, cmd)
     if isinstance(rsp, str):
         req.respond(HTTP_OK, HGTYPE, body=rsp)
@@ -103,10 +169,16 @@  def call(repo, req, cmd):
         else:
             gen = rsp.gen
 
-        if rsp.v1compressible:
-            gen = p.compresschunks(gen)
+        # This code for compression should not be streamres specific. It
+        # is here because we only compress streamres at the moment.
+        mediatype, engine, engineopts = p.responsetype(rsp.v1compressible)
 
-        req.respond(HTTP_OK, HGTYPE)
+        if mediatype == HGTYPE and rsp.v1compressible:
+            gen = engine.compressstream(gen, engineopts)
+        elif mediatype == HGTYPE2:
+            gen = genversion2(gen, rsp.v1compressible, engine, engineopts)
+
+        req.respond(HTTP_OK, mediatype)
         return gen
     elif isinstance(rsp, wireproto.pushres):
         val = p.restore()
diff --git a/tests/get-with-headers.py b/tests/get-with-headers.py
--- a/tests/get-with-headers.py
+++ b/tests/get-with-headers.py
@@ -35,6 +35,13 @@  if '--json' in sys.argv:
     sys.argv.remove('--json')
     formatjson = True
 
+accept = None
+if '--accept' in sys.argv:
+    idx = sys.argv.index('--accept')
+    accept = sys.argv[idx + 1]
+    sys.argv.pop(idx)
+    sys.argv.pop(idx)
+
 tag = None
 def request(host, path, show):
     assert not path.startswith('/'), path
@@ -42,6 +49,8 @@  def request(host, path, show):
     headers = {}
     if tag:
         headers['If-None-Match'] = tag
+    if accept:
+        headers['Accept'] = accept
 
     conn = httplib.HTTPConnection(host)
     conn.request("GET", '/' + path, None, headers)
diff --git a/tests/test-http-protocol.t b/tests/test-http-protocol.t
--- a/tests/test-http-protocol.t
+++ b/tests/test-http-protocol.t
@@ -43,3 +43,134 @@  Order of engines can also change
   compression=UN,ZL
 
   $ killdaemons.py
+
+Start a default server again
+
+  $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+Server should send application/mercurial-0.1 to clients if no Accept is used
+
+  $ get-with-headers.py --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
+  200 Script output follows
+  content-type: application/mercurial-0.1
+  date: * (glob)
+  server: * (glob)
+  transfer-encoding: chunked
+
+Server should send application/mercurial-0.1 when client sends it
+
+  $ get-with-headers.py --accept 'application/mercurial-0.1' --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
+  200 Script output follows
+  content-type: application/mercurial-0.1
+  date: * (glob)
+  server: * (glob)
+  transfer-encoding: chunked
+
+Server should send application/mercurial-0.2 when client sends it
+
+  $ get-with-headers.py --accept 'application/mercurial-0.2' --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
+  200 Script output follows
+  content-type: application/mercurial-0.2
+  date: * (glob)
+  server: * (glob)
+  transfer-encoding: chunked
+
+  $ get-with-headers.py --accept 'application/mercurial-0.1; q=0.5, application/mercurial-0.2' --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
+  200 Script output follows
+  content-type: application/mercurial-0.2
+  date: * (glob)
+  server: * (glob)
+  transfer-encoding: chunked
+
+Requesting a compression format that server doesn't support results will fall back to 0.1
+
+  $ get-with-headers.py --accept 'application/mercurial-0.2; comp="AA"' --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' -
+  200 Script output follows
+  content-type: application/mercurial-0.1
+  date: * (glob)
+  server: * (glob)
+  transfer-encoding: chunked
+
+#if zstd
+zstd is used if available
+
+  $ get-with-headers.py --accept 'application/mercurial-0.2; comp="ZS"' 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
+  $ f --size --hexdump --bytes 33 --sha1 resp
+  resp: size=245, sha1=821812c6bdb516252b2cefb76d2effd5e
+  0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
+  0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 5a 53 28 b5 2f |t follows..ZS(./|
+  0020: fd                                              |.|
+
+Variation with no space between ";" and "comp="
+
+  $ get-with-headers.py --accept 'application/mercurial-0.2;comp="ZS"' 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' > resp
+  $ f --size --hexdump --bytes 33 --sha1 resp
+  resp: size=245, sha1=821812c6bdb516252b2cefb76d2effd5e
+  0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
+  0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 5a 53 28 b5 2f |t follows..ZS(./|
+  0020: fd                                              |.|
+
+#endif
+
+application/mercurial-0.2 is not yet used on non-streaming responses
+
+  $ get-with-headers.py --accept 'application/mercurial-0.2' 127.0.0.1:$HGPORT '?cmd=heads' -
+  200 Script output follows
+  content-length: 41
+  content-type: application/mercurial-0.1
+  date: * (glob)
+  server: * (glob)
+  
+  e93700bd72895c5addab234c56d4024b487a362f
+
+Now test protocol preference usage
+
+  $ killdaemons.py
+  $ hg --config server.compressionengines=none,zlib -R server serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+No Accept will send 0.1+zlib, even though "none" is preferred b/c "none" isn't supported on 0.1
+
+  $ get-with-headers.py --headeronly 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000' Content-Type
+  200 Script output follows
+  content-type: application/mercurial-0.1
+
+  $ get-with-headers.py 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000'  > resp
+  $ f --size --hexdump --bytes 28 --sha1 resp
+  resp: size=227, sha1=35a4c074da74f32f5440da3cbf04
+  0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
+  0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 78             |t follows..x|
+
+Explicit 0.1 will send zlib because "none" isn't supported on 0.1
+
+  $ get-with-headers.py --accept 'application/mercurial-0.1' 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000'  > resp
+  $ f --size --hexdump --bytes 28 --sha1 resp
+  resp: size=227, sha1=35a4c074da74f32f5440da3cbf04
+  0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
+  0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 78             |t follows..x|
+
+0.2 with no compression will get "none" because that is server's preference
+(spec says ZL and UN are implicitly supported)
+
+  $ get-with-headers.py --accept 'application/mercurial-0.2' 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000'  > resp
+  $ f --size --hexdump --bytes 29 --sha1 resp
+  resp: size=429, sha1=3378b4413c610e34292dad2554da8
+  0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
+  0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 55 4e          |t follows..UN|
+
+Client receives server preference even if local order doesn't match
+
+  $ get-with-headers.py --accept 'application/mercurial-0.2; comp="ZL UN"' 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000'  > resp
+  $ f --size --hexdump --bytes 29 --sha1 resp
+  resp: size=429, sha1=3378b4413c610e34292dad2554da8
+  0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
+  0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 55 4e          |t follows..UN|
+
+Client receives only supported format even if not server preferred format
+
+  $ get-with-headers.py --accept 'application/mercurial-0.2; comp="ZL"' 127.0.0.1:$HGPORT '?cmd=getbundle&heads=e93700bd72895c5addab234c56d4024b487a362f&common=0000000000000000000000000000000000000000'  > resp
+  $ f --size --hexdump --bytes 30 --sha1 resp
+  resp: size=229, sha1=af91407ec1f4950ac78bc3e31f18c0
+  0000: 32 30 30 20 53 63 72 69 70 74 20 6f 75 74 70 75 |200 Script outpu|
+  0010: 74 20 66 6f 6c 6c 6f 77 73 0a 0a 5a 4c 78       |t follows..ZLx|