In 23fc8e4f61 (refs: implement batch reference update support, 2025-04-08) we introduced a mechanism to batch reference updates. The idea being that we wanted to batch updates similar to a transaction, but allow certain updates to fail. This would help reference backends optimize such operations and also remove the overhead of processing updates individually. Especially for the reftable backend, where batching updates would ensure that the autocompaction would only occur at the end of the batch instead of after every reference update. As of 9d2962a7c4 (receive-pack: use batched reference updates, 2025-05-19) we also updated the 'git-fetch(1)' and 'git-receive-pack(1)' command to use batched reference updates. This series fixes some bugs that we found at GitLab by running our Gitaly service with the `next` build of Git. The first being in the files backend, which missed skipping over failed updates in certain flows. When certain individual updates fail, we mark them as such. However, we missed skipping over such failed updates, which can cause a SEGFAULT. The other is in the git-receive-pack(1) implementation when a user pushes multiple branches such as: delete refs/heads/branch/conflict create refs/heads/branch Before using batched updates, the references would be applied sequentially and hence no conflicts would arise. With batched updates, while the first update applies, the second fails due to F/D conflict. A similar issue was present in 'git-fetch(1)' and was fixed by using separating out reference pruning into a separate transaction. Apply a similar mechanism for 'git-receive-pack(1)' and separate out reference deletions into its own batch. This is based off master 7014b55638 (A bit more topics for -rc1, 2025-05-30), with the changes from kn/fetch-push-bulk-ref-update merged in. --- Changes in v3: - Cleanup the second commit message and remove stale comments. - In the second commit, only create the transaction if needed. - Link to v2: https://lore.kernel.org/r/20250605-6769-address-test-failures-in-the-next-branch-caused-by-batched-reference-updates-v2-0-26cd05b8a79e@xxxxxxxxx Changes in v2: - Modify the test in the first commit to no longer do a quiet grep, and some more tests. - Remove the second commit as it was unnecessary. - Modify the commit message in the last commit, to also talk about how we now use 2 transactions at minimum but this is still better than before. - Modify the logic in the last commit to no longer use an XOR and instead loop over the two different scenarios (deletion updates, other updates). - Link to v1: https://lore.kernel.org/r/20250602-6769-address-test-failures-in-the-next-branch-caused-by-batched-reference-updates-v1-0-903d1db3f10e@xxxxxxxxx --- builtin/receive-pack.c | 100 ++++++++++++++++++++++++++++++++----------------- refs/files-backend.c | 7 ++++ t/t1400-update-ref.sh | 45 ++++++++++++++++++++++ t/t5516-fetch-push.sh | 17 +++++++-- 4 files changed, 131 insertions(+), 38 deletions(-) Karthik Nayak (2): refs/files: skip updates with errors in batched updates receive-pack: handle reference deletions separately Range-diff versus v2: 1: 50f318714b = 1: 695cc3b9d4 refs/files: skip updates with errors in batched updates 2: d7293b64b0 ! 2: a20bb05dd6 receive-pack: handle reference deletions separately @@ Commit message similar mechanism for 'git-receive-pack(1)' and separate out reference deletions into its own batch. - This means 'git-receive-pack(1)' will now use exactly two transactions, + This means 'git-receive-pack(1)' will now use upto two transactions, whereas before using batched updates it would use _at least_ two - transactions. So using batched updates is the still the better option. + transactions. So using batched updates is still the better option. Add a test to validate this behavior. @@ builtin/receive-pack.c: static void execute_commands_non_atomic(struct command * - for (cmd = commands; cmd; cmd = cmd->next) { - if (!should_process_cmd(cmd) || cmd->run_proc_receive) - continue; -+ for (int phase = PHASE_DELETIONS; phase <= PHASE_OTHERS; phase++) { -+ transaction = ref_store_transaction_begin(get_main_ref_store(the_repository), -+ REF_TRANSACTION_ALLOW_FAILURE, &err); -+ if (!transaction) { -+ rp_error("%s", err.buf); -+ strbuf_reset(&err); -+ reported_error = "transaction failed to s1tart"; -+ goto failure; -+ } ++ for (enum processing_phase phase = PHASE_DELETIONS; phase <= PHASE_OTHERS; phase++) { ++ for (cmd = commands; cmd; cmd = cmd->next) { ++ if (!should_process_cmd(cmd) || cmd->run_proc_receive) ++ continue; - cmd->error_string = update(cmd, si); - } -+ for (cmd = commands; cmd; cmd = cmd->next) { -+ if (!should_process_cmd(cmd) || cmd->run_proc_receive) ++ if (phase == PHASE_DELETIONS && !is_null_oid(&cmd->new_oid)) ++ continue; ++ else if (phase == PHASE_OTHERS && is_null_oid(&cmd->new_oid)) + continue; - if (ref_transaction_commit(transaction, &err)) { @@ builtin/receive-pack.c: static void execute_commands_non_atomic(struct command * - reported_error = "failed to update refs"; - goto failure; - } -+ if (phase == PHASE_DELETIONS && !is_null_oid(&cmd->new_oid)) -+ continue; -+ else if (phase == PHASE_OTHERS && is_null_oid(&cmd->new_oid)) -+ continue; ++ /* ++ * Lazily create a transaction only when we know there are ++ * updates to be added. ++ */ ++ if (!transaction) { ++ transaction = ref_store_transaction_begin(get_main_ref_store(the_repository), ++ REF_TRANSACTION_ALLOW_FAILURE, &err); ++ if (!transaction) { ++ rp_error("%s", err.buf); ++ strbuf_reset(&err); ++ reported_error = "transaction failed to s1tart"; ++ goto failure; ++ } ++ } - ref_transaction_for_each_rejected_update(transaction, - ref_transaction_rejection_handler, @@ builtin/receive-pack.c: static void execute_commands_non_atomic(struct command * - if (strmap_empty(&failed_refs)) - goto cleanup; -+ if (ref_transaction_commit(transaction, &err)) { -+ rp_error("%s", err.buf); -+ reported_error = "failed to update refs"; -+ goto failure; -+ } ++ /* ++ * If no transaction was created, there is nothing to commit. ++ */ ++ if (!transaction) ++ goto cleanup; -failure: - for (cmd = commands; cmd; cmd = cmd->next) { @@ builtin/receive-pack.c: static void execute_commands_non_atomic(struct command * - else if (strmap_contains(&failed_refs, cmd->ref_name)) - cmd->error_string = strmap_get(&failed_refs, cmd->ref_name); - } -+ ref_transaction_for_each_rejected_update(transaction, -+ ref_transaction_rejection_handler, -+ &failed_refs); ++ if (ref_transaction_commit(transaction, &err)) { ++ rp_error("%s", err.buf); ++ reported_error = "failed to update refs"; ++ goto failure; ++ } -cleanup: - ref_transaction_free(transaction); - strmap_clear(&failed_refs, 0); - strbuf_release(&err); ++ ref_transaction_for_each_rejected_update(transaction, ++ ref_transaction_rejection_handler, ++ &failed_refs); ++ + if (strmap_empty(&failed_refs)) + goto cleanup; + @@ builtin/receive-pack.c: static void execute_commands_non_atomic(struct command * + + cleanup: + ref_transaction_free(transaction); ++ transaction = NULL; + strmap_clear(&failed_refs, 0); + strbuf_release(&err); + } } static void execute_commands_atomic(struct command *commands, -@@ builtin/receive-pack.c: static void execute_commands(struct command *commands, - /* - * If there is no command ready to run, should return directly to destroy - * temporary data in the quarantine area. -+ * -+ * Check if any reference deletions exist, these are batched together in -+ * a separate transaction to avoid F/D conflicts with other updates. - */ - for (cmd = commands; cmd && cmd->error_string; cmd = cmd->next) - ; /* nothing */ - - ## t/t1416-ref-transaction-hooks.sh ## -@@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'interleaving hook calls succeed' ' - EOF - - cat >expect <<-EOF && -+ hooks/reference-transaction prepared -+ hooks/reference-transaction committed - hooks/update refs/tags/PRE $ZERO_OID $PRE_OID - hooks/update refs/tags/POST $ZERO_OID $POST_OID - hooks/reference-transaction prepared ## t/t5516-fetch-push.sh ## @@ t/t5516-fetch-push.sh: test_expect_success 'pushing valid refs triggers post-receive and post-update ho base-commit: 931c39f05e078e0df968a439379cb04b5c4666ef change-id: 20250528-6769-address-test-failures-in-the-next-branch-caused-by-batched-reference-updates-24ff53680144 Thanks - Karthik