~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http/__init__.py

  • Committer: Vincent Ladeuil
  • Date: 2007-06-20 13:56:21 UTC
  • mto: (2574.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 2575.
  • Revision ID: v.ladeuil+lp@free.fr-20070620135621-x43c0hnmzu0iuo6m
Fix #115209 by issuing a single range request on 400: Bad Request

* bzrlib/transport/http/response.py:
(handle_response): Consider 400 as an indication that too much
ranges were specified.

* bzrlib/transport/http/_urllib2_wrappers.py:
(Request): Add an 'accpeted_errors' parameters describing what
error codes the caller will handle.
(HTTPErrorProcessor): Mention that Request specific accepted error
codes takes precedence.
(HTTPDefaultErrorHandler.http_error_default): Remove dead code.

* bzrlib/transport/http/_urllib.py:
(HttpTransport_urllib._get): Add 400 as an accepted error iff
ranges are specified.
(HttpTransport_urllib._head): Restrict accepted errors.

* bzrlib/transport/http/__init__.py:
(HttpTransportBase._degrade_range_hint,
HttpTransportBase._get_ranges_hinted): Replace _retry_get.
(HttpTransportBase.readv): Simplified and avoid the spurious _get()
issued when _get was successful.

* bzrlib/tests/test_http.py:
(TestLimitedRangeRequestServer,
TestLimitedRangeRequestServer_urllib,
TestLimitedRangeRequestServer_pycurl): Bug #115209 specific tests.

* bzrlib/tests/HTTPTestUtil.py:
(LimitedRangeRequestHandler, LimitedRangeHTTPServer): New test
classes to emulate apache throwing 400: Bad Request when too much
ranges are specified.
(AuthRequestHandler.do_GET): Remove dead code. Yeah, I know,
not related to the bug :-/

Show diffs side-by-side

added added

removed removed

Lines of Context:
255
255
        """
256
256
        return self
257
257
 
258
 
    def _retry_get(self, relpath, ranges, exc_info):
259
 
        """A GET request have failed, let's retry with a simpler request."""
260
 
 
261
 
        try_again = False
262
 
        # The server does not gives us enough data or
263
 
        # a bogus-looking result, let's try again with
264
 
        # a simpler request if possible.
 
258
    def _degrade_range_hint(self, relpath, ranges, exc_info):
265
259
        if self._range_hint == 'multi':
266
260
            self._range_hint = 'single'
267
 
            mutter('Retry %s with single range request' % relpath)
268
 
            try_again = True
 
261
            mutter('Retry "%s" with single range request' % relpath)
269
262
        elif self._range_hint == 'single':
270
263
            self._range_hint = None
271
 
            mutter('Retry %s without ranges' % relpath)
272
 
            try_again = True
273
 
        if try_again:
274
 
            # Note that since the offsets and the ranges may not
275
 
            # be in the same order, we don't try to calculate a
276
 
            # restricted single range encompassing unprocessed
277
 
            # offsets.
278
 
            code, f = self._get(relpath, ranges)
279
 
            return try_again, code, f
 
264
            mutter('Retry "%s" without ranges' % relpath)
280
265
        else:
281
 
            # We tried all the tricks, but nothing worked. We
282
 
            # re-raise original exception; the 'mutter' calls
283
 
            # above will indicate that further tries were
284
 
            # unsuccessful
 
266
            # We tried all the tricks, but nothing worked. We re-raise original
 
267
            # exception; the 'mutter' calls above will indicate that further
 
268
            # tries were unsuccessful
285
269
            raise exc_info[0], exc_info[1], exc_info[2]
286
270
 
287
 
    # Having to round trip to the server means waiting for a response, so it is
288
 
    # better to download extra bytes. We have two constraints to satisfy when
289
 
    # choosing the right value: a request with too much ranges will make some
290
 
    # servers issue a '400: Bad request' error (when we have too much ranges,
291
 
    # we exceed the apache header max size (8190 by default) for example) ;
292
 
    # coalescing too much ranges will increasing the data transferred and
293
 
    # degrade performances.
294
 
    _bytes_to_read_before_seek = 512
295
 
    # No limit on the offset number combined into one, we are trying to avoid
296
 
    # downloading the whole file.
 
271
    def _get_ranges_hinted(self, relpath, ranges):
 
272
        """Issue a ranged GET request taking server capabilities into account.
 
273
 
 
274
        Depending of the errors returned by the server, we try several GET
 
275
        requests, trying to minimize the data transferred.
 
276
 
 
277
        :param relpath: Path relative to transport base URL
 
278
        :param ranges: None to get the whole file;
 
279
            or  a list of _CoalescedOffset to fetch parts of a file.
 
280
        :returns: A file handle containing at least the requested ranges.
 
281
        """
 
282
        exc_info = None
 
283
        try_again = True
 
284
        while try_again:
 
285
            try_again = False
 
286
            try:
 
287
                code, f = self._get(relpath, ranges)
 
288
            except errors.InvalidRange, e:
 
289
                if exc_info is None:
 
290
                    exc_info = sys.exc_info()
 
291
                self._degrade_range_hint(relpath, ranges, exc_info)
 
292
                try_again = True
 
293
        return f
 
294
 
 
295
    # _coalesce_offsets is a helper for readv, it try to combine ranges without
 
296
    # degrading readv performances. _bytes_to_read_before_seek is the value
 
297
    # used for the limit parameter and has been tuned for other transports. For
 
298
    # HTTP, the name is inappropriate but the parameter is still useful and
 
299
    # helps reduce the number of chunks in the response. The overhead for a
 
300
    # chunk (headers, length, footer around the data itself is variable but
 
301
    # around 50 bytes. We use 128 to reduce the range specifiers that appear in
 
302
    # the header, some servers (notably Apache) enforce a maximum length for a
 
303
    # header and issue a '400: Bad request' error when too much ranges are
 
304
    # specified.
 
305
    _bytes_to_read_before_seek = 128
 
306
    # No limit on the offset number that get combined into one, we are trying
 
307
    # to avoid downloading the whole file.
297
308
    _max_readv_combined = 0
298
309
 
299
310
    def readv(self, relpath, offsets):
311
322
        mutter('http readv of %s  offsets => %s collapsed %s',
312
323
                relpath, len(offsets), len(coalesced))
313
324
 
314
 
        try_again = True
315
 
        while try_again:
316
 
            try_again = False
317
 
            try:
318
 
                code, f = self._get(relpath, coalesced)
319
 
            except (errors.InvalidRange, errors.ShortReadvError), e:
320
 
                try_again, code, f = self._retry_get(relpath, coalesced,
321
 
                                                     sys.exc_info())
322
 
 
 
325
        f = self._get_ranges_hinted(relpath, coalesced)
323
326
        for start, size in offsets:
324
327
            try_again = True
325
328
            while try_again:
331
334
                    if len(data) != size:
332
335
                        raise errors.ShortReadvError(relpath, start, size,
333
336
                                                     actual=len(data))
334
 
                except (errors.InvalidRange, errors.ShortReadvError), e:
335
 
                    # Note that we replace 'f' here and that it
336
 
                    # may need cleaning one day before being
337
 
                    # thrown that way.
338
 
                    try_again, code, f = self._retry_get(relpath, coalesced,
339
 
                                                         sys.exc_info())
 
337
                except errors.ShortReadvError, e:
 
338
                    self._degrade_range_hint(relpath, coalesced, sys.exc_info())
 
339
 
 
340
                    # Since the offsets and the ranges may not be in the same
 
341
                    # order, we don't try to calculate a restricted single
 
342
                    # range encompassing unprocessed offsets.
 
343
 
 
344
                    # Note: we replace 'f' here, it may need cleaning one day
 
345
                    # before being thrown that way.
 
346
                    f = self._get_ranges_hinted(relpath, coalesced)
 
347
                    try_again = True
 
348
 
340
349
            # After one or more tries, we get the data.
341
350
            yield start, data
342
351