refs/files-backend: handle packed transaction prepare failure

In files_transaction_prepare(), if we have to delete some refs, we use a
subordinate packed_transaction to do so. It's rare for that
sub-transaction's prepare step to fail, since we hold the packed-refs
lock. But if it does, we trigger a BUG() due to these steps:

  - we've attached the packed transaction to the files transaction as
    backend_data->packed_transaction

  - when the prepare step fails, the packed transaction cleans itself
    up, putting itself into the CLOSED state

  - the error value from preparing the packed transaction lets us know
    in files_transaction_prepare() that we should also clean up and
    return an error. We call files_transaction_cleanup(), which tries to
    abort backend_data->packed_transaction. Since it's already CLOSED,
    that triggers an assertion in ref_transaction_abort().

We can fix that by disconnecting the packed transaction from the outer
files transaction, and then free-ing (not aborting!) it ourselves.

A few other options/alternatives I considered:

  - we could just make it a noop to abort a CLOSED transaction. But that
    seems less safe, since clearly this code expects (and enforces) a
    particular set of state transitions.

  - we could have files_transaction_cleanup() selectively call abort()
    vs free() based on the state of the on the packed transaction.
    That's basically a more restricted version of the above, but also
    potentially unsafe.

  - instead of disconnecting backend_data->packed_transaction on error,
    we could wait to install it until we successfully prepare. That
    might make the flow a little simpler, but it introduces a hassle.
    Earlier parts of files_transaction_prepare() that encounter an error
    will jump to the cleanup label, and expect that cleaning up the
    outer transaction will clean up the packed transaction, too. We'd
    have to adjust those sites to clean up the packed transaction.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Jeff King 2019-03-21 05:28:44 -04:00 коммит произвёл Junio C Hamano
Родитель aeb582a983
Коммит 249e8dc73e
2 изменённых файлов: 26 добавлений и 0 удалений

Просмотреть файл

@ -2696,6 +2696,16 @@ static int files_transaction_prepare(struct ref_store *ref_store,
if (is_packed_transaction_needed(refs->packed_ref_store,
packed_transaction)) {
ret = ref_transaction_prepare(packed_transaction, err);
/*
* A failure during the prepare step will abort
* itself, but not free. Do that now, and disconnect
* from the files_transaction so it does not try to
* abort us when we hit the cleanup code below.
*/
if (ret) {
ref_transaction_free(packed_transaction);
backend_data->packed_transaction = NULL;
}
} else {
/*
* We can skip rewriting the `packed-refs`

Просмотреть файл

@ -618,4 +618,20 @@ test_expect_success 'delete fails cleanly if packed-refs file is locked' '
test_cmp unchanged actual
'
test_expect_success 'delete fails cleanly if packed-refs.new write fails' '
# Setup and expectations are similar to the test above.
prefix=refs/failed-packed-refs &&
git update-ref $prefix/foo $C &&
git pack-refs --all &&
git update-ref $prefix/foo $D &&
git for-each-ref $prefix >unchanged &&
# This should not happen in practice, but it is an easy way to get a
# reliable error (we open with create_tempfile(), which uses O_EXCL).
: >.git/packed-refs.new &&
test_when_finished "rm -f .git/packed-refs.new" &&
test_must_fail git update-ref -d $prefix/foo &&
git for-each-ref $prefix >actual &&
test_cmp unchanged actual
'
test_done