git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH v2 1/1] maintenance: use systemd timers on Linux

From
PWPhillip Wood <phillip.wood123@gmail.com>
Date
May 10, 2021, 18:03 UTC
Message-ID
<3fd17223-8667-24be-2e65-f1970d411bdf@gmail.com>
In-Reply-To
<20210509213217.449489-2-lenaic@lhuard.fr>
Hi Lénaïc

Thanks for updating the patch, I've left some comments below. Aside from one possible bug, they mostly revolve around not passing the name of scheduler command around when that name is fixed and functions which do not check for errors but return a success/failure value (either they should check the return values of the system calls they make or be declared as void if it is safe to ignore the failures)

On 09/05/2021 22:32, Lénaïc Huard wrote:
Show 30 quoted lines
> The existing mechanism for scheduling background maintenance is done
> through cron. On Linux systems managed by systemd, systemd provides an
> alternative to schedule recurring tasks: systemd timers.
> 
> The main motivations to implement systemd timers in addition to cron
> are:
> * cron is optional and Linux systems running systemd might not have it
>    installed.
> * The execution of `crontab -l` can tell us if cron is installed but not
>    if the daemon is actually running.
> * With systemd, each service is run in its own cgroup and its logs are
>    tagged by the service inside journald. With cron, all scheduled tasks
>    are running in the cron daemon cgroup and all the logs of the
>    user-scheduled tasks are pretended to belong to the system cron
>    service.
>    Concretely, a user that doesn’t have access to the system logs won’t
>    have access to the log of its own tasks scheduled by cron whereas he
>    will have access to the log of its own tasks scheduled by systemd
>    timer.
>    Although `cron` attempts to send email, that email may go unseen by
>    the user because these days, local mailboxes are not heavily used
>    anymore.
> 
> In order to choose which scheduler to use between `cron` and user
> systemd timers, a new option
> `--scheduler=auto|cron|systemd|launchctl|schtasks` has been added to
> `git maintenance start`.
> When `git maintenance start --scheduler=XXX` is run, it not only
> registers `git maintenance run` tasks in the scheduler XXX, it also
> removes the `git maintenance run` tasks from all the other schedulers

I'm not sure it is actually doing that at the moment - see my comment in update_background_schedule()

Show 9 quoted lines
> to
> ensure we cannot have two schedulers launching concurrent identical
> tasks.
> 
> The default value is `auto` which chooses a suitable scheduler for the
> system.
> On Linux, if user systemd timers are available, they will be used as git
> maintenance scheduler. If not, `cron` will be used if it is available.
> If none is available, it will fail.

I think defaulting to systemd timers when both systemd and cron are installed is sensible given the problems associated with testing whether crond is actually running in that scenario.

Show 125 quoted lines
> `git maintenance stop` doesn't have any `--scheduler` parameter because
> this command will try to remove the `git maintenance run` tasks from all
> the available schedulers.
> 
> In order to schedule git maintenance, we need two unit template files:
> * ~/.config/systemd/user/git-maintenance@.service
>    to define the command to be started by systemd and
> * ~/.config/systemd/user/git-maintenance@.timer
>    to define the schedule at which the command should be run.
> 
> Those units are templates that are parametrized by the frequency.
> 
> Based on those templates, 3 timers are started:
> * git-maintenance@hourly.timer
> * git-maintenance@daily.timer
> * git-maintenance@weekly.timer
> 
> The command launched by those three timers are the same than with the
> other scheduling methods:
> 
> git for-each-repo --config=maintenance.repo maintenance run
> --schedule=%i
> 
> with the full path for git to ensure that the version of git launched
> for the scheduled maintenance is the same as the one used to run
> `maintenance start`.
> 
> The timer unit contains `Persistent=true` so that, if the computer is
> powered down when a maintenance task should run, the task will be run
> when the computer is back powered on.
> 
> Signed-off-by: Lénaïc Huard <lenaic@lhuard.fr>
> ---
>   Documentation/git-maintenance.txt |  60 +++++
>   builtin/gc.c                      | 375 ++++++++++++++++++++++++++++--
>   t/t7900-maintenance.sh            |  51 ++++
>   3 files changed, 462 insertions(+), 24 deletions(-)
> 
> diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt
> index 80ddd33ceb..f012923333 100644
> --- a/Documentation/git-maintenance.txt
> +++ b/Documentation/git-maintenance.txt
> @@ -181,6 +181,20 @@ OPTIONS
>   	`maintenance.<task>.enabled` configured as `true` are considered.
>   	See the 'TASKS' section for the list of accepted `<task>` values.
>   
> +--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::
> +	When combined with the `start` subcommand, specify the scheduler
> +	to use to run the hourly, daily and weekly executions of
> +	`git maintenance run`.
> +	The possible values for `<scheduler>` depend on the system: `crontab`
> +	is available on POSIX systems, `systemd-timer` is available on Linux
> +	systems, `launchctl` is available on MacOS and `schtasks` is available
> +	on Windows.
> +	By default or when `auto` is specified, the most appropriate scheduler
> +	for the system is used. On MacOS, `launchctl` is used. On Windows,
> +	`schtasks` is used. On Linux, `systemd-timers` is used if user systemd
> +	timers are available, otherwise, `crontab` is used. On all other systems,
> +	`crontab` is used.
> +
>   
>   TROUBLESHOOTING
>   ---------------
> @@ -279,6 +293,52 @@ schedule to ensure you are executing the correct binaries in your
>   schedule.
>   
>   
> +BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS
> +-----------------------------------------------
> +
> +While Linux supports `cron`, depending on the distribution, `cron` may
> +be an optional package not necessarily installed. On modern Linux
> +distributions, systemd timers are superseding it.
> +
> +If user systemd timers are available, they will be used as a replacement
> +of `cron`.
> +
> +In this case, `git maintenance start` will create user systemd timer units
> +and start the timers. The current list of user-scheduled tasks can be found
> +by running `systemctl --user list-timers`. The timers written by `git
> +maintenance start` are similar to this:
> +
> +-----------------------------------------------------------------------
> +$ systemctl --user list-timers
> +NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES
> +Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service
> +Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service
> +Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service
> +-----------------------------------------------------------------------
> +
> +One timer is registered for each `--schedule=<frequency>` option.
> +
> +The definition of the systemd units can be inspected in the following files:
> +
> +-----------------------------------------------------------------------
> +~/.config/systemd/user/git-maintenance@.timer
> +~/.config/systemd/user/git-maintenance@.service
> +~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer
> +~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer
> +~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer
> +-----------------------------------------------------------------------
> +
> +`git maintenance start` will overwrite these files and start the timer
> +again with `systemctl --user`, so any customization should be done by
> +creating a drop-in file, i.e. a `.conf` suffixed file in the
> +`~/.config/systemd/user/git-maintenance@.service.d` directory.
> +
> +`git maintenance stop` will stop the user systemd timers and delete
> +the above mentioned files.
> +
> +For more details, see systemd.timer(5)
> +
> +
>   BACKGROUND MAINTENANCE ON MACOS SYSTEMS
>   ---------------------------------------
>   
> diff --git a/builtin/gc.c b/builtin/gc.c
> index ef7226d7bc..7c72aa3b99 100644
> --- a/builtin/gc.c
> +++ b/builtin/gc.c
> @@ -1544,6 +1544,15 @@ static const char *get_frequency(enum schedule_priority schedule)
>   	}
>   }
>   
> +static int is_launchctl_available(const char *cmd)

None of these is_..._available() function needs the cmd parameter. It matches the existing pattern of the existing ..._update_schedule() functions but they don't really need the cmd argument either so I would drop the argument for the new functions.

Show 78 quoted lines
> +{
> +#ifdef __APPLE__
> +	return 1;
> +#else
> +	return 0;
> +#endif
> +}
> +
>   static char *launchctl_service_name(const char *frequency)
>   {
>   	struct strbuf label = STRBUF_INIT;
> @@ -1710,6 +1719,15 @@ static int launchctl_update_schedule(int run_maintenance, int fd, const char *cm
>   		return launchctl_remove_plists(cmd);
>   }
>   
> +static int is_schtasks_available(const char *cmd)
> +{
> +#ifdef GIT_WINDOWS_NATIVE
> +	return 1;
> +#else
> +	return 0;
> +#endif
> +}
> +
>   static char *schtasks_task_name(const char *frequency)
>   {
>   	struct strbuf label = STRBUF_INIT;
> @@ -1872,6 +1890,28 @@ static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd
>   		return schtasks_remove_tasks(cmd);
>   }
>   
> +static int is_crontab_available(const char *cmd)
> +{
> +	static int cached_result = -1;
> +	struct child_process child = CHILD_PROCESS_INIT;
> +
> +	if (cached_result != -1)
> +		return cached_result;
> +
> +	strvec_split(&child.args, cmd);
> +	strvec_push(&child.args, "-l");
> +	child.no_stdin = 1;
> +	child.no_stdout = 1;
> +	child.no_stderr = 1;
> +	child.silent_exec_failure = 1;
> +
> +	if (start_command(&child))
> +		return cached_result = 0;
> +	/* Ignore exit code, as an empty crontab will return error. */
> +	finish_command(&child);
> +	return cached_result = 1;
> +}
> +
>   #define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
>   #define END_LINE "# END GIT MAINTENANCE SCHEDULE"
>   
> @@ -1959,61 +1999,348 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)
>   	return result;
>   }
>   
> +static int is_systemd_timer_available(const char *cmd)
> +{
> +#ifdef __linux__
> +	static int cached_result = -1;
> +	struct child_process child = CHILD_PROCESS_INIT;
> +
> +	if (cached_result != -1)
> +		return cached_result;
> +
> +	strvec_split(&child.args, cmd);
> +	strvec_pushl(&child.args, "--user", "list-timers", NULL);
> +	child.no_stdin = 1;
> +	child.no_stdout = 1;
> +	child.no_stderr = 1;
> +	child.silent_exec_failure = 1;
> +
> +	if (start_command(&child))
> +		return cached_result = 0;

This is maybe a bit too clever. It would be clearer to separate the assignment of the cached result and the return statement.

Show 23 quoted lines
> +	if (finish_command(&child))
> +		return cached_result = 0;
> +	return cached_result = 1;
> +#else
> +	return 0;
> +#endif
> +}
> +
> +static char *xdg_config_home_systemd(const char *filename)
> +{
> +	const char *home, *config_home;
> +
> +	assert(filename);
> +	config_home = getenv("XDG_CONFIG_HOME");
> +	if (config_home && *config_home)
> +		return mkpathdup("%s/systemd/user/%s", config_home, filename);
> +
> +	home = getenv("HOME");
> +	if (home)
> +		return mkpathdup("%s/.config/systemd/user/%s", home, filename);
> +
> +	die(_("failed to get $XDG_CONFIG_HOME and $HOME"));
> +}

This is largely a copy of xdg_config_home(), I would prefer to see this function calling that rather than duplicating it.

static char *xdg_config_home_systemd(const char *filename)
{
     struct strbuf buf = STRBUF_INIT;
     char *path;
     strbuf_addf(&buf, "systemd/user/%s", filename);
     path = xdg_config_home(buf.buf);
     strbuf_release(&buf);
     return path;
}
Even better would be to modify xdg_config_home() to take a format string.
> +static int systemd_timer_enable_unit(int enable,
> +				     enum schedule_priority schedule,
> +				     const char *cmd)

The cmd argument is pointless, it will always be "systemctl" and you have even hard coded that value into the error message below.

Show 12 quoted lines
> +{
> +	struct child_process child = CHILD_PROCESS_INIT;
> +	const char *frequency = get_frequency(schedule);
> +
> +	strvec_split(&child.args, cmd);
> +	strvec_pushl(&child.args, "--user", enable ? "enable" : "disable",
> +		     "--now", NULL);
> +	strvec_pushf(&child.args, "git-maintenance@%s.timer", frequency);
> +
> +	if (start_command(&child))
> +		die(_("failed to run systemctl"));
> +	return finish_command(&child);

There is a strange mix of dying and returning an error here. Also should this be printing a message if finish_command() returns a non-zero value? If systemctl fails then there is not much we can do to recover so dying on any error might be ok unless we need to perform some cleanup if it fails like removing the timer unit files.

What is the exit code of systemctl if a unit is already enabled and we try to enbale it again (and the same for disabling a disabled unit)?

Show 6 quoted lines
> +}
> +
> +static int systemd_timer_delete_unit_templates(void)
> +{
> +	char *filename = xdg_config_home_systemd("git-maintenance@.timer");
> +	unlink(filename);

This is missing error handling (and below). If there is a good reason to ignore the errors then there is no point in returning a value from this function

Show 47 quoted lines
> +	free(filename);
> +
> +	filename = xdg_config_home_systemd("git-maintenance@.service");
> +	unlink(filename);
> +	free(filename);
> +
> +	return 0;
> +}
> +
> +static int systemd_timer_delete_units(const char *cmd)
> +{
> +	return systemd_timer_enable_unit(0, SCHEDULE_HOURLY, cmd) ||
> +	       systemd_timer_enable_unit(0, SCHEDULE_DAILY, cmd) ||
> +	       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, cmd) ||
> +	       systemd_timer_delete_unit_templates();
> +}
> +
> +static int systemd_timer_write_unit_templates(const char *exec_path)
> +{
> +	char *filename;
> +	FILE *file;
> +	const char *unit;
> +
> +	filename = xdg_config_home_systemd("git-maintenance@.timer");
> +	if (safe_create_leading_directories(filename))
> +		die(_("failed to create directories for '%s'"), filename);
> +	file = xfopen(filename, "w");
> +	free(filename);
> +
> +	unit = "# This file was created and is maintained by Git.\n"
> +	       "# Any edits made in this file might be replaced in the future\n"
> +	       "# by a Git command.\n"
> +	       "\n"
> +	       "[Unit]\n"
> +	       "Description=Optimize Git repositories data\n"
> +	       "\n"
> +	       "[Timer]\n"
> +	       "OnCalendar=%i\n"
> +	       "Persistent=true\n"
> +	       "\n"
> +	       "[Install]\n"
> +	       "WantedBy=timers.target\n";
> +	fputs(unit, file);
> +	fclose(file);
> +
> +	filename = xdg_config_home_systemd("git-maintenance@.service");
> +	if (safe_create_leading_directories(filename))

Haven't we already created this path if it was missing above for the first file?

> +		die(_("failed to create directories for '%s'"), filename);
> +	file = xfopen(filename, "w");

Do we want to try and remove the first file if we cannot write the second file to leave the file system in a consitent state? If so you'll need to use something like fopen_or_warn() and check the return value.

Show 19 quoted lines
> +	free(filename);
> +
> +	unit = "# This file was created and is maintained by Git.\n"
> +	       "# Any edits made in this file might be replaced in the future\n"
> +	       "# by a Git command.\n"
> +	       "\n"
> +	       "[Unit]\n"
> +	       "Description=Optimize Git repositories data\n"
> +	       "\n"
> +	       "[Service]\n"
> +	       "Type=oneshot\n"
> +	       "ExecStart=\"%1$s/git\" --exec-path=\"%1$s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\n"
> +	       "LockPersonality=yes\n"
> +	       "MemoryDenyWriteExecute=yes\n"
> +	       "NoNewPrivileges=yes\n"
> +	       "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\n"
> +	       "RestrictNamespaces=yes\n"
> +	       "RestrictRealtime=yes\n"
> +	       "RestrictSUIDSGID=yes\n"

After a quick read of the systemd.exec man page it is unclear to me if these Restrict... lines are needed as we already have NoNewPrivileges=yes - maybe they have some effect if `git maintence` is run as root?

Show 6 quoted lines
> +	       "SystemCallArchitectures=native\n"
> +	       "SystemCallFilter=@system-service\n";
> +	fprintf(file, unit, exec_path);
> +	fclose(file);
> +
> +	return 0;

As we don't return any errors but die instead there is no need to return a value. On the other hand maybe we should be checking the return values of fclose() and possibly fputs() / fprint().

Show 10 quoted lines
> +}
> +
> +static int systemd_timer_setup_units(const char *cmd)
> +{
> +	const char *exec_path = git_exec_path();
> +
> +	return systemd_timer_write_unit_templates(exec_path) ||
> +	       systemd_timer_enable_unit(1, SCHEDULE_HOURLY, cmd) ||
> +	       systemd_timer_enable_unit(1, SCHEDULE_DAILY, cmd) ||
> +	       systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, cmd);

If any step fails do we need to try and reverse the preceding steps to avoid leaving so units enabled but not others. In practice this is probably pretty unlikely so maybe we don't need to worry.

> +}
> +
> +static int systemd_timer_update_schedule(int run_maintenance, int fd,
> +					 const char *cmd)
No need for the cmd argument
Show 57 quoted lines
> +{
> +	if (run_maintenance)
> +		return systemd_timer_setup_units(cmd);
> +	else
> +		return systemd_timer_delete_units(cmd);
> +}
> +
> +enum scheduler {
> +	SCHEDULER_INVALID = -1,
> +	SCHEDULER_AUTO = 0,
> +	SCHEDULER_CRON = 1,
> +	SCHEDULER_SYSTEMD = 2,
> +	SCHEDULER_LAUNCHCTL = 3,
> +	SCHEDULER_SCHTASKS = 4,
> +};
> +
> +static const struct {
> +	int (*is_available)(const char *cmd);
> +	int (*update_schedule)(int run_maintenance, int fd, const char *cmd);
> +	const char *cmd;
> +} scheduler_fn[] = {
> +	[SCHEDULER_CRON] = { is_crontab_available, crontab_update_schedule,
> +			     "crontab" },
> +	[SCHEDULER_SYSTEMD] = { is_systemd_timer_available,
> +				systemd_timer_update_schedule, "systemctl" },
> +	[SCHEDULER_LAUNCHCTL] = { is_launchctl_available,
> +				  launchctl_update_schedule, "launchctl" },
> +	[SCHEDULER_SCHTASKS] = { is_schtasks_available,
> +				 schtasks_update_schedule, "schtasks" },
> +};
> +
> +static enum scheduler parse_scheduler(const char *value)
> +{
> +	if (!value)
> +		return SCHEDULER_INVALID;
> +	else if (!strcasecmp(value, "auto"))
> +		return SCHEDULER_AUTO;
> +	else if (!strcasecmp(value, "cron") || !strcasecmp(value, "crontab"))
> +		return SCHEDULER_CRON;
> +	else if (!strcasecmp(value, "systemd") ||
> +		 !strcasecmp(value, "systemd-timer"))
> +		return SCHEDULER_SYSTEMD;
> +	else if (!strcasecmp(value, "launchctl"))
> +		return SCHEDULER_LAUNCHCTL;
> +	else if (!strcasecmp(value, "schtasks"))
> +		return SCHEDULER_SCHTASKS;
> +	else
> +		return SCHEDULER_INVALID;
> +}
> +
> +static int maintenance_opt_scheduler(const struct option *opt, const char *arg,
> +				     int unset)
> +{
> +	enum scheduler *scheduler = opt->value;
> +
> +	if (unset)
> +		die(_("--no-scheduler is not allowed"));

As others have said it would be better to BUG() here and pass PARSE_OPT_NONEG when defining the option below.

Show 110 quoted lines
> +
> +	*scheduler = parse_scheduler(arg);
> +
> +	if (*scheduler == SCHEDULER_INVALID)
> +		die(_("unrecognized --scheduler argument '%s'"), arg);
> +
> +	return 0;
> +}
> +
> +struct maintenance_start_opts {
> +	enum scheduler scheduler;
> +};
> +
> +static void resolve_auto_scheduler(enum scheduler *scheduler)
> +{
> +	if (*scheduler != SCHEDULER_AUTO)
> +		return;
> +
>   #if defined(__APPLE__)
> -static const char platform_scheduler[] = "launchctl";
> +	*scheduler = SCHEDULER_LAUNCHCTL;
> +	return;
> +
>   #elif defined(GIT_WINDOWS_NATIVE)
> -static const char platform_scheduler[] = "schtasks";
> +	*scheduler = SCHEDULER_SCHTASKS;
> +	return;
> +
> +#elif defined(__linux__)
> +	if (is_systemd_timer_available("systemctl"))
> +		*scheduler = SCHEDULER_SYSTEMD;
> +	else if (is_crontab_available("crontab"))
> +		*scheduler = SCHEDULER_CRON;
> +	else
> +		die(_("neither systemd timers nor crontab are available"));
> +	return;
> +
>   #else
> -static const char platform_scheduler[] = "crontab";
> +	*scheduler = SCHEDULER_CRON;
> +	return;
>   #endif
> +}
>   
> -static int update_background_schedule(int enable)
> +static void validate_scheduler(enum scheduler scheduler)
>   {
> -	int result;
> -	const char *scheduler = platform_scheduler;
> -	const char *cmd = scheduler;
> +	const char *cmd;
> +
> +	if (scheduler == SCHEDULER_INVALID)
> +		BUG("invalid scheduler");
> +	if (scheduler == SCHEDULER_AUTO)
> +		BUG("resolve_auto_scheduler should have been called before");
> +
> +	cmd = scheduler_fn[scheduler].cmd;
> +	if (!scheduler_fn[scheduler].is_available(cmd))
> +		die(_("%s scheduler is not available"), cmd);
> +}
> +
> +static int update_background_schedule(const struct maintenance_start_opts *opts,
> +				      int enable)
> +{
> +	unsigned int i;
> +	int res, result = 0;
> +	enum scheduler scheduler;
> +	const char *cmd = NULL;
>   	char *testing;
>   	struct lock_file lk;
>   	char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path);
>   
> +	if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)
> +		return error(_("another process is scheduling background maintenance"));
> +
>   	testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER"));
>   	if (testing) {
>   		char *sep = strchr(testing, ':');
>   		if (!sep)
>   			die("GIT_TEST_MAINT_SCHEDULER unparseable: %s", testing);
>   		*sep = '\0';
> -		scheduler = testing;
> +		scheduler = parse_scheduler(testing);
>   		cmd = sep + 1;
> +		result = scheduler_fn[scheduler].update_schedule(
> +			enable, get_lock_file_fd(&lk), cmd);
> +		goto done;
>   	}
>   
> -	if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)
> -		return error(_("another process is scheduling background maintenance"));
> -
> -	if (!strcmp(scheduler, "launchctl"))
> -		result = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);
> -	else if (!strcmp(scheduler, "schtasks"))
> -		result = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);
> -	else if (!strcmp(scheduler, "crontab"))
> -		result = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);
> -	else
> -		die("unknown background scheduler: %s", scheduler);
> +	for (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {
> +		int enable_scheduler = enable && (opts->scheduler == i);
> +		cmd = scheduler_fn[i].cmd;
> +		if (!scheduler_fn[i].is_available(cmd))
> +			continue;
> +		res = scheduler_fn[i].update_schedule(
> +			enable_scheduler, get_lock_file_fd(&lk), cmd);
> +		if (enable_scheduler)
> +			result = res;

If I have understood the code correctly then if systemd timers are currently enabled and the user runs `git maintenance --scheduler=cron` then cron will be enabled and the loop will quit before we get the the entry to disable systemd timers leaving both running. I think it would be cleaner and clearer to loop over the schedulers disabling them first and then enable the one the user has selected.

Show 16 quoted lines
> +	}
>   
> +done:
>   	rollback_lock_file(&lk);
>   	free(testing);
>   	return result;
>   }
>   
> -static int maintenance_start(void)
> +static const char *const builtin_maintenance_start_usage[] = {
> +	N_("git maintenance start [--scheduler=<scheduler>]"), NULL
> +};
> +
> +static int maintenance_start(int argc, const char **argv, const char *prefix)
>   {
> +	struct maintenance_start_opts opts;
struct maintenance_start_opts opts = { 0 };
would avoid the need for the memset() call below
Thanks again for working on this. Best Wishes
Phillip
Show 113 quoted lines
> +	struct option builtin_maintenance_start_options[] = {
> +		OPT_CALLBACK(
> +			0, "scheduler", &opts.scheduler, N_("scheduler"),
> +			N_("scheduler to use to trigger git maintenance run"),
> +			maintenance_opt_scheduler),
> +		OPT_END()
> +	};
> +	memset(&opts, 0, sizeof(opts));
> +
> +	argc = parse_options(argc, argv, prefix,
> +			     builtin_maintenance_start_options,
> +			     builtin_maintenance_start_usage, 0);
> +
> +	resolve_auto_scheduler(&opts.scheduler);
> +	validate_scheduler(opts.scheduler);
> +
> +	if (argc > 0)
> +		usage_with_options(builtin_maintenance_start_usage,
> +				   builtin_maintenance_start_options);
> +
>   	if (maintenance_register())
>   		warning(_("failed to add repo to global config"));
> -
> -	return update_background_schedule(1);
> +	return update_background_schedule(&opts, 1);
>   }
>   
>   static int maintenance_stop(void)
>   {
> -	return update_background_schedule(0);
> +	return update_background_schedule(NULL, 0);
>   }
>   
>   static const char builtin_maintenance_usage[] =	N_("git maintenance <subcommand> [<options>]");
> @@ -2027,7 +2354,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)
>   	if (!strcmp(argv[1], "run"))
>   		return maintenance_run(argc - 1, argv + 1, prefix);
>   	if (!strcmp(argv[1], "start"))
> -		return maintenance_start();
> +		return maintenance_start(argc - 1, argv + 1, prefix);
>   	if (!strcmp(argv[1], "stop"))
>   		return maintenance_stop();
>   	if (!strcmp(argv[1], "register"))
> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
> index 2412d8c5c0..6e6316cd90 100755
> --- a/t/t7900-maintenance.sh
> +++ b/t/t7900-maintenance.sh
> @@ -20,6 +20,20 @@ test_xmllint () {
>   	fi
>   }
>   
> +test_lazy_prereq SYSTEMD_ANALYZE '
> +	systemd-analyze --help >out &&
> +	grep verify out
> +'
> +
> +test_systemd_analyze_verify () {
> +	if test_have_prereq SYSTEMD_ANALYZE
> +	then
> +		systemd-analyze verify "$@"
> +	else
> +		true
> +	fi
> +}
> +
>   test_expect_success 'help text' '
>   	test_expect_code 129 git maintenance -h 2>err &&
>   	test_i18ngrep "usage: git maintenance <subcommand>" err &&
> @@ -615,6 +629,43 @@ test_expect_success 'start and stop Windows maintenance' '
>   	test_cmp expect args
>   '
>   
> +test_expect_success 'start and stop Linux/systemd maintenance' '
> +	write_script print-args <<-\EOF &&
> +	echo $* >>args
> +	EOF
> +
> +	rm -f args &&
> +	GIT_TEST_MAINT_SCHEDULER="systemd:./print-args" git maintenance start &&
> +
> +	# start registers the repo
> +	git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
> +
> +	test_systemd_analyze_verify "$HOME/.config/systemd/user/git-maintenance@.service" &&
> +
> +	rm -f expect &&
> +	for frequency in hourly daily weekly
> +	do
> +		echo "--user enable --now git-maintenance@${frequency}.timer" >>expect || return 1
> +	done &&
> +	test_cmp expect args &&
> +
> +	rm -f args &&
> +	GIT_TEST_MAINT_SCHEDULER="systemd:./print-args" git maintenance stop &&
> +
> +	# stop does not unregister the repo
> +	git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
> +
> +	test_path_is_missing "$HOME/.config/systemd/user/git-maintenance@.timer" &&
> +	test_path_is_missing "$HOME/.config/systemd/user/git-maintenance@.service" &&
> +
> +	rm -f expect &&
> +	for frequency in hourly daily weekly
> +	do
> +		echo "--user disable --now git-maintenance@${frequency}.timer" >>expect || return 1
> +	done &&
> +	test_cmp expect args
> +'
> +
>   test_expect_success 'register preserves existing strategy' '
>   	git config maintenance.strategy none &&
>   	git maintenance register &&
> 
Previous: Đoàn Trần Công DanhNext: Eric Sunshine
Message 24 of 138 in “maintenance: use systemd timers on Linux”
  1. maintenance: use systemd timers on LinuxLénaïc Huard, May 1, 2021
  2. brian m. carlsonMay 1, 2021
  3. Bagas SanjayaMay 2, 2021
  4. Eric SunshineMay 2, 2021
  5. Eric SunshineMay 2, 2021
  6. Phillip WoodMay 2, 2021
  7. Đoàn Trần Công DanhMay 5, 2021
  8. Phillip WoodMay 5, 2021
  9. Ævar Arnfjörð BjarmasonMay 5, 2021
  10. Lénaïc HuardMay 9, 2021
  11. Ævar Arnfjörð BjarmasonMay 10, 2021
  12. Bagas SanjayaMay 2, 2021
  13. Derrick StoleeMay 3, 2021
  14. 0/1 maintenance: use systemd timers on LinuxLénaïc Huard, May 9, 2021
  15. 1/1 maintenance: use systemd timers on LinuxLénaïc Huard, May 9, 2021
  16. Đoàn Trần Công DanhMay 10, 2021
  17. Eric SunshineMay 10, 2021
  18. Junio C HamanoMay 10, 2021
  19. Đoàn Trần Công DanhMay 12, 2021
  20. Felipe ContrerasMay 12, 2021
  21. Phillip WoodMay 12, 2021
  22. Phillip WoodMay 12, 2021
  23. Đoàn Trần Công DanhMay 12, 2021
  24. Phillip WoodMay 10, 2021
  25. Eric SunshineMay 10, 2021
  26. Phillip WoodMay 10, 2021
  27. Eric SunshineMay 10, 2021
  28. Lénaïc HuardJun 8, 2021
  29. Martin ÅgrenMay 10, 2021
  30. Phillip WoodMay 11, 2021
  31. Derrick StoleeMay 11, 2021
  32. 0/4 maintenance: use systemd timers on LinuxLénaïc Huard, May 20, 2021
  33. 2/4 maintenance: introduce ENABLE/DISABLE for code clarityLénaïc Huard, May 20, 2021
  34. 4/4 maintenance: optionally use systemd timers on LinuxLénaïc Huard, May 20, 2021
  35. Bagas SanjayaMay 21, 2021
  36. Derrick StoleeMay 21, 2021
  37. Johannes SchindelinMay 22, 2021
  38. Felipe ContrerasMay 23, 2021
  39. brian m. carlsonMay 23, 2021
  40. Felipe ContrerasMay 24, 2021
  41. Ævar Arnfjörð BjarmasonMay 24, 2021
  42. Junio C HamanoMay 24, 2021
  43. Johannes SchindelinMay 25, 2021
  44. Felipe ContrerasMay 25, 2021
  45. CoC, inclusivity etc. (was "Re: [...] systemd timers on Linux")Ævar Arnfjörð Bjarmason, May 26, 2021
  46. Felipe ContrerasMay 26, 2021
  47. Jeff KingMay 27, 2021
  48. Felipe ContrerasMay 27, 2021
  49. Junio C HamanoMay 27, 2021
  50. Phillip SusiMay 28, 2021
  51. Jeff KingMay 30, 2021
  52. Felipe ContrerasMay 24, 2021
  53. 1/4 cache.h: rename "xdg_config_home" to "xdg_config_home_git"Lénaïc Huard, May 20, 2021
  54. Đoàn Trần Công DanhMay 20, 2021
  55. 3/4 maintenance: `git maintenance run` learned `--scheduler=<scheduler>`Lénaïc Huard, May 20, 2021
  56. Bagas SanjayaMay 21, 2021
  57. 0/4 add support for systemd timers on LinuxLénaïc Huard, May 24, 2021
  58. 4/4 maintenance: add support for systemd timers on LinuxLénaïc Huard, May 24, 2021
  59. Ævar Arnfjörð BjarmasonMay 24, 2021
  60. Eric SunshineMay 24, 2021
  61. Felipe ContrerasMay 24, 2021
  62. Phillip WoodMay 26, 2021
  63. 3/4 maintenance: `git maintenance run` learned `--scheduler=<scheduler>`Lénaïc Huard, May 24, 2021
  64. Phillip WoodMay 24, 2021
  65. Lénaïc HuardMay 30, 2021
  66. Phillip WoodMay 30, 2021
  67. 2/4 maintenance: introduce ENABLE/DISABLE for code clarityLénaïc Huard, May 24, 2021
  68. Phillip WoodMay 24, 2021
  69. Đoàn Trần Công DanhMay 24, 2021
  70. Lénaïc HuardMay 25, 2021
  71. Junio C HamanoMay 25, 2021
  72. Ævar Arnfjörð BjarmasonMay 24, 2021
  73. 1/4 cache.h: Introduce a generic "xdg_config_home_for(…)" functionLénaïc Huard, May 24, 2021
  74. Phillip WoodMay 24, 2021
  75. Đoàn Trần Công DanhMay 24, 2021
  76. Junio C HamanoMay 24, 2021
  77. 0/3 add support for systemd timers on LinuxLénaïc Huard, Jun 8, 2021
  78. 3/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Jun 8, 2021
  79. Jeff KingJun 9, 2021
  80. Phillip WoodJun 9, 2021
  81. 1/3 cache.h: Introduce a generic "xdg_config_home_for(…)" functionLénaïc Huard, Jun 8, 2021
  82. 2/3 maintenance: `git maintenance run` learned `--scheduler=<scheduler>`Lénaïc Huard, Jun 8, 2021
  83. Junio C HamanoJun 9, 2021
  84. Phillip WoodJun 9, 2021
  85. 0/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Jun 12, 2021
  86. 1/3 cache.h: Introduce a generic "xdg_config_home_for(…)" functionLénaïc Huard, Jun 12, 2021
  87. 3/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Jun 12, 2021
  88. 2/3 maintenance: `git maintenance run` learned `--scheduler=<scheduler>`Lénaïc Huard, Jun 12, 2021
  89. Eric SunshineJun 14, 2021
  90. Derrick StoleeJun 16, 2021
  91. Eric SunshineJun 17, 2021
  92. Phillip WoodJun 17, 2021
  93. Lénaïc HuardJul 2, 2021
  94. 0/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Jul 2, 2021
  95. 1/3 cache.h: Introduce a generic "xdg_config_home_for(…)" functionLénaïc Huard, Jul 2, 2021
  96. 3/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Jul 2, 2021
  97. Ævar Arnfjörð BjarmasonJul 6, 2021
  98. 2/3 maintenance: `git maintenance run` learned `--scheduler=<scheduler>`Lénaïc Huard, Jul 2, 2021
  99. Ævar Arnfjörð BjarmasonJul 6, 2021
  100. Junio C HamanoJul 6, 2021
  101. Jeff KingJul 13, 2021
  102. Eric SunshineJul 13, 2021
  103. Jeff KingJul 13, 2021
  104. Eric SunshineJul 13, 2021
  105. Bagas SanjayaJul 13, 2021
  106. Felipe ContrerasJul 6, 2021
  107. Lénaïc HuardAug 23, 2021
  108. Junio C HamanoAug 23, 2021
  109. Junio C HamanoJul 2, 2021
  110. Phillip WoodJul 6, 2021
  111. 0/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Aug 23, 2021
  112. 3/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Aug 23, 2021
  113. Derrick StoleeAug 24, 2021
  114. 1/3 cache.h: Introduce a generic "xdg_config_home_for(…)" functionLénaïc Huard, Aug 23, 2021
  115. 2/3 maintenance: `git maintenance run` learned `--scheduler=<scheduler>`Lénaïc Huard, Aug 23, 2021
  116. Derrick StoleeAug 24, 2021
  117. Derrick StoleeAug 24, 2021
  118. 0/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Aug 27, 2021
  119. 1/3 cache.h: Introduce a generic "xdg_config_home_for(…)" functionLénaïc Huard, Aug 27, 2021
  120. 2/3 maintenance: `git maintenance run` learned `--scheduler=<scheduler>`Lénaïc Huard, Aug 27, 2021
  121. Ramsay JonesAug 27, 2021
  122. 3/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Aug 27, 2021
  123. 0/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Sep 4, 2021
  124. 1/3 cache.h: Introduce a generic "xdg_config_home_for(…)" functionLénaïc Huard, Sep 4, 2021
  125. 3/3 maintenance: add support for systemd timers on LinuxLénaïc Huard, Sep 4, 2021
  126. 2/3 maintenance: `git maintenance run` learned `--scheduler=<scheduler>`Lénaïc Huard, Sep 4, 2021
  127. Derrick StoleeSep 7, 2021
  128. Derrick StoleeSep 8, 2021
  129. Lénaïc HuardSep 9, 2021
  130. Derrick StoleeSep 9, 2021
  131. Ævar Arnfjörð BjarmasonSep 27, 2021
  132. Lénaïc HuardSep 27, 2021
  133. Derrick StoleeAug 17, 2021
  134. Phillip WoodAug 17, 2021
  135. Derrick StoleeAug 17, 2021
  136. Lénaïc HuardAug 18, 2021
  137. Derrick StoleeAug 18, 2021
  138. Junio C HamanoAug 18, 2021

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.