comparison mercurial/bundle2.py @ 32024:ad41739c6b2b

bundle2: ignore errors seeking a bundle after an exception (issue4784) Many have seen a "stream ended unexpectedly" error. This message is raised from changegroup.readexactly() when a read(n) operation fails to return exactly N bytes. I believe most occurrences of this error in the wild stem from the code changed in this patch. Before, if bundle2's part applicator raised an Exception when processing/applying parts, the exception handler would attempt to iterate the remaining parts. If I/O during this iteration failed, it would likely raise the "stream ended unexpectedly" exception. The problem with this approach is that if we already encountered an I/O error iterating the bundle2 data during application, then any further I/O would almost certainly fail. If the original stream were closed, changegroup.readexactly() would obtain an empty string, triggering "stream ended unexpectedly" with "got 0." This is the error message that users would see. What's worse is that the original I/O related exception would be lost since a new exception would be raised. This made debugging the actual I/O failure effectively impossible. This patch changes the exception handler for bundle2 application to ignore errors when seeking the underlying stream. When the underlying error is I/O related, the seek should fail fast and the original exception will be re-raised. The output changes in test-http-bad-server.t demonstrate this. When the underlying error is not I/O related and the stream can be seeked, the old behavior is preserved.
author Gregory Szorc <gregory.szorc@gmail.com>
date Sun, 16 Apr 2017 11:55:08 -0700
parents a02e773008f5
children 99515353c72a 76f9a0009b4b
comparison
equal deleted inserted replaced
32023:a29580905771 32024:ad41739c6b2b
352 nbpart = 0 352 nbpart = 0
353 try: 353 try:
354 for nbpart, part in iterparts: 354 for nbpart, part in iterparts:
355 _processpart(op, part) 355 _processpart(op, part)
356 except Exception as exc: 356 except Exception as exc:
357 for nbpart, part in iterparts: 357 # Any exceptions seeking to the end of the bundle at this point are
358 # consume the bundle content 358 # almost certainly related to the underlying stream being bad.
359 part.seek(0, 2) 359 # And, chances are that the exception we're handling is related to
360 # getting in that bad state. So, we swallow the seeking error and
361 # re-raise the original error.
362 seekerror = False
363 try:
364 for nbpart, part in iterparts:
365 # consume the bundle content
366 part.seek(0, 2)
367 except Exception:
368 seekerror = True
369
360 # Small hack to let caller code distinguish exceptions from bundle2 370 # Small hack to let caller code distinguish exceptions from bundle2
361 # processing from processing the old format. This is mostly 371 # processing from processing the old format. This is mostly
362 # needed to handle different return codes to unbundle according to the 372 # needed to handle different return codes to unbundle according to the
363 # type of bundle. We should probably clean up or drop this return code 373 # type of bundle. We should probably clean up or drop this return code
364 # craziness in a future version. 374 # craziness in a future version.
368 if op.reply is not None: 378 if op.reply is not None:
369 salvaged = op.reply.salvageoutput() 379 salvaged = op.reply.salvageoutput()
370 replycaps = op.reply.capabilities 380 replycaps = op.reply.capabilities
371 exc._replycaps = replycaps 381 exc._replycaps = replycaps
372 exc._bundle2salvagedoutput = salvaged 382 exc._bundle2salvagedoutput = salvaged
373 raise 383
384 # Re-raising from a variable loses the original stack. So only use
385 # that form if we need to.
386 if seekerror:
387 raise exc
388 else:
389 raise
374 finally: 390 finally:
375 repo.ui.debug('bundle2-input-bundle: %i parts total\n' % nbpart) 391 repo.ui.debug('bundle2-input-bundle: %i parts total\n' % nbpart)
376 392
377 return op 393 return op
378 394