builtin/maintenance: split into foreground and background tasks

Both git-gc(1) and git-maintenance(1) have logic to daemonize so that the maintenance tasks are performed in the background. git-gc(1) has some special logic though to not perform _all_ housekeeping tasks in the background: both references and reflogs are still handled synchronously in the foreground. This split exists because otherwise it may easily happen that git-gc(1) keeps the "packed-refs" file locked for an extended amount of time, where the next Git command that wants to modify any reference could now fail. This was especially important in the past, where git-gc(1) was still executed directly as part of our automatic maintenance: git-gc(1) was invoked via `git gc --auto --detach`, so we knew to handle most of the maintenance tasks in the background while doing those parts that may cause locking issues in the foreground. We have since moved to git-maintenance(1), which is a more flexible replacement for git-gc(1). By default this command runs git-gc(1), only, but it can be configured to run different tasks, as well. This command does not know about the split between maintenance tasks that should run before and after detach though, and this has led to several bug reports about spurious locking errors for the "packed-refs" file. Prepare for a fix by introducing this split for maintenance tasks. Note that this commit does not yet change any of the tasks, so there should not (yet) be a change in behaviour. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jun 3, 2025 at 16:01 UTC 5bb4298acfb57550934cefca85101825eff177e9
1 file changed +49 -21
builtin/gc.c
+49 -21
@@ -1535,84 +1535,106 @@ static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts
1535
1536 typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,
1537 struct gc_config *cfg);
1538 -
1539 -/*
1540 - * An auto condition function returns 1 if the task should run
1541 - * and 0 if the task should NOT run. See needs_to_gc() for an
1542 - * example.
1543 - */
1538 typedef int (*maintenance_auto_fn)(struct gc_config *cfg);
1539
1540 struct maintenance_task {
1541 const char *name;
1548 - maintenance_task_fn fn;
1542 +
1543 + /*
1544 + * Work that will be executed before detaching. This should not include
1545 + * tasks that may run for an extended amount of time as it does cause
1546 + * auto-maintenance to block until foreground tasks have been run.
1547 + */
1548 + maintenance_task_fn foreground;
1549 +
1550 + /*
1551 + * Work that will be executed after detaching. When not detaching the
1552 + * work will be run in the foreground, as well.
1553 + */
1554 + maintenance_task_fn background;
1555 +
1556 + /*
1557 + * An auto condition function returns 1 if the task should run and 0 if
1558 + * the task should NOT run. See needs_to_gc() for an example.
1559 + */
1560 maintenance_auto_fn auto_condition;
1561 };
1562
1563 static const struct maintenance_task tasks[] = {
1564 [TASK_PREFETCH] = {
1565 .name = "prefetch",
1555 - .fn = maintenance_task_prefetch,
1566 + .background = maintenance_task_prefetch,
1567 },
1568 [TASK_LOOSE_OBJECTS] = {
1569 .name = "loose-objects",
1559 - .fn = maintenance_task_loose_objects,
1570 + .background = maintenance_task_loose_objects,
1571 .auto_condition = loose_object_auto_condition,
1572 },
1573 [TASK_INCREMENTAL_REPACK] = {
1574 .name = "incremental-repack",
1564 - .fn = maintenance_task_incremental_repack,
1575 + .background = maintenance_task_incremental_repack,
1576 .auto_condition = incremental_repack_auto_condition,
1577 },
1578 [TASK_GC] = {
1579 .name = "gc",
1569 - .fn = maintenance_task_gc,
1580 + .background = maintenance_task_gc,
1581 .auto_condition = need_to_gc,
1582 },
1583 [TASK_COMMIT_GRAPH] = {
1584 .name = "commit-graph",
1574 - .fn = maintenance_task_commit_graph,
1585 + .background = maintenance_task_commit_graph,
1586 .auto_condition = should_write_commit_graph,
1587 },
1588 [TASK_PACK_REFS] = {
1589 .name = "pack-refs",
1579 - .fn = maintenance_task_pack_refs,
1590 + .background = maintenance_task_pack_refs,
1591 .auto_condition = pack_refs_condition,
1592 },
1593 [TASK_REFLOG_EXPIRE] = {
1594 .name = "reflog-expire",
1584 - .fn = maintenance_task_reflog_expire,
1595 + .background = maintenance_task_reflog_expire,
1596 .auto_condition = reflog_expire_condition,
1597 },
1598 [TASK_WORKTREE_PRUNE] = {
1599 .name = "worktree-prune",
1589 - .fn = maintenance_task_worktree_prune,
1600 + .background = maintenance_task_worktree_prune,
1601 .auto_condition = worktree_prune_condition,
1602 },
1603 [TASK_RERERE_GC] = {
1604 .name = "rerere-gc",
1594 - .fn = maintenance_task_rerere_gc,
1605 + .background = maintenance_task_rerere_gc,
1606 .auto_condition = rerere_gc_condition,
1607 },
1608 };
1609
1610 +enum task_phase {
1611 + TASK_PHASE_FOREGROUND,
1612 + TASK_PHASE_BACKGROUND,
1613 +};
1614 +
1615 static int maybe_run_task(const struct maintenance_task *task,
1616 struct repository *repo,
1617 struct maintenance_run_opts *opts,
1602 - struct gc_config *cfg)
1618 + struct gc_config *cfg,
1619 + enum task_phase phase)
1620 {
1621 + int foreground = (phase == TASK_PHASE_FOREGROUND);
1622 + maintenance_task_fn fn = foreground ? task->foreground : task->background;
1623 + const char *region = foreground ? "maintenance foreground" : "maintenance";
1624 int ret = 0;
1625
1626 + if (!fn)
1627 + return 0;
1628 if (opts->auto_flag &&
1629 (!task->auto_condition || !task->auto_condition(cfg)))
1630 return 0;
1631
1610 - trace2_region_enter("maintenance", task->name, repo);
1611 - if (task->fn(opts, cfg)) {
1632 + trace2_region_enter(region, task->name, repo);
1633 + if (fn(opts, cfg)) {
1634 error(_("task '%s' failed"), task->name);
1635 ret = 1;
1636 }
1615 - trace2_region_leave("maintenance", task->name, repo);
1637 + trace2_region_leave(region, task->name, repo);
1638
1639 return ret;
1640 }
@@ -1641,6 +1663,11 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,
1663 }
1664 free(lock_path);
1665
1666 + for (size_t i = 0; i < opts->tasks_nr; i++)
1667 + if (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,
1668 + TASK_PHASE_FOREGROUND))
1669 + result = 1;
1670 +
1671 /* Failure to daemonize is ok, we'll continue in foreground. */
1672 if (opts->detach > 0) {
1673 trace2_region_enter("maintenance", "detach", the_repository);
@@ -1649,7 +1676,8 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,
1676 }
1677
1678 for (size_t i = 0; i < opts->tasks_nr; i++)
1652 - if (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg))
1679 + if (maybe_run_task(&tasks[opts->tasks[i]], r, opts, cfg,
1680 + TASK_PHASE_BACKGROUND))
1681 result = 1;
1682
1683 rollback_lock_file(&lk);