Skip to content

Commit 77529c4

Browse files
committed
maintenance: use random minute in systemd scheduler
The get_random_minute() method was created to allow maintenance schedules to be fixed to a random minute of the hour. This randomness is only intended to spread out the load from a number of clients, but each client should have an hour between each maintenance cycle. Add this random minute to the systemd integration. This integration is more complicated than similar changes for other schedulers because of a neat trick that systemd allows: templating. The previous implementation generated two template files with names of the form 'git-maintenance@.(timer|service)'. The '.timer' or '.service' indicates that this is a template that is picked up when we later specify '...@<schedule>.timer' or '...@<schedule>.service'. The '<schedule>' string is then used to insert into the template both the 'OnCalendar' schedule setting and the '--schedule' parameter of the 'git maintenance run' command. In order to set these schedules to a given minute, we can no longer use the 'hourly', 'daily', or 'weekly' strings for '<schedule>' and instead need to abandon the template model for the .timer files. We can still use templates for the .service files. For this reason, we split these writes into two methods. Modify the template with a custom schedule in the 'OnCalendar' setting. This schedule has some interesting differences from cron-like patterns, but is relatively easy to figure out from context. The one that might be confusing is that '*-*-*' is a date-based pattern, but this must be omitted when using 'Mon' to signal that we care about the day of the week. Monday is used since that matches the day used for the 'weekly' schedule used previously. Now that the timer files are not templates, we might want to abandon the '@' symbol in the file names. However, this would cause users with existing schedules to get two competing schedules due to different names. The work to remove the old schedule name is one thing that we can avoid by keeping the '@' symbol in our unit names. Since we are locked into this name, it makes sense that we keep the template model for the .service files. The rest of the change involves making sure we are writing these .timer and .service files before initializing the schedule with 'systemctl' and deleting the files when we are done. Some changes are also made to share the random minute along with a single computation of the execution path of the current Git executable. In addition, older Git versions may have written a '[email protected]' template file. Be sure to remove this when successfully enabling maintenance (or disabling maintenance). Signed-off-by: Derrick Stolee <[email protected]>
1 parent 16f5836 commit 77529c4

File tree

2 files changed

+142
-25
lines changed

2 files changed

+142
-25
lines changed

builtin/gc.c

Lines changed: 129 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2335,29 +2335,54 @@ static char *xdg_config_home_systemd(const char *filename)
23352335
return xdg_config_home_for("systemd/user", filename);
23362336
}
23372337

2338-
static int systemd_timer_delete_unit_templates(void)
2338+
#define SYSTEMD_UNIT_FORMAT "git-maintenance@%s.%s"
2339+
2340+
static int systemd_timer_delete_timer_file(enum schedule_priority priority)
23392341
{
23402342
int ret = 0;
2341-
char *filename = xdg_config_home_systemd("[email protected]");
2343+
const char *frequency = get_frequency(priority);
2344+
char *local_timer_name = xstrfmt(SYSTEMD_UNIT_FORMAT, frequency, "timer");
2345+
char *filename = xdg_config_home_systemd(local_timer_name);
2346+
23422347
if (unlink(filename) && !is_missing_file_error(errno))
23432348
ret = error_errno(_("failed to delete '%s'"), filename);
2344-
FREE_AND_NULL(filename);
23452349

2346-
filename = xdg_config_home_systemd("[email protected]");
2350+
free(filename);
2351+
free(local_timer_name);
2352+
return ret;
2353+
}
2354+
2355+
static int systemd_timer_delete_service_template(void)
2356+
{
2357+
int ret = 0;
2358+
char *local_service_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "service");
2359+
char *filename = xdg_config_home_systemd(local_service_name);
23472360
if (unlink(filename) && !is_missing_file_error(errno))
23482361
ret = error_errno(_("failed to delete '%s'"), filename);
23492362

23502363
free(filename);
2364+
free(local_service_name);
23512365
return ret;
23522366
}
23532367

2354-
static int systemd_timer_write_unit_templates(const char *exec_path)
2368+
/*
2369+
* Write the schedule information into a git-maintenance@<schedule>.timer
2370+
* file using a custom minute. This timer file cannot use the templating
2371+
* system, so we generate a specific file for each.
2372+
*/
2373+
static int systemd_timer_write_timer_file(enum schedule_priority schedule,
2374+
int minute)
23552375
{
2376+
int res = -1;
23562377
char *filename;
23572378
FILE *file;
23582379
const char *unit;
2380+
char *schedule_pattern = NULL;
2381+
const char *frequency = get_frequency(schedule);
2382+
char *local_timer_name = xstrfmt(SYSTEMD_UNIT_FORMAT, frequency, "timer");
2383+
2384+
filename = xdg_config_home_systemd(local_timer_name);
23592385

2360-
filename = xdg_config_home_systemd("[email protected]");
23612386
if (safe_create_leading_directories(filename)) {
23622387
error(_("failed to create directories for '%s'"), filename);
23632388
goto error;
@@ -2366,6 +2391,23 @@ static int systemd_timer_write_unit_templates(const char *exec_path)
23662391
if (!file)
23672392
goto error;
23682393

2394+
switch (schedule) {
2395+
case SCHEDULE_HOURLY:
2396+
schedule_pattern = xstrfmt("*-*-* *:%02d:00", minute);
2397+
break;
2398+
2399+
case SCHEDULE_DAILY:
2400+
schedule_pattern = xstrfmt("*-*-* 0:%02d:00", minute);
2401+
break;
2402+
2403+
case SCHEDULE_WEEKLY:
2404+
schedule_pattern = xstrfmt("Mon 0:%02d:00", minute);
2405+
break;
2406+
2407+
default:
2408+
BUG("Unhandled schedule_priority");
2409+
}
2410+
23692411
unit = "# This file was created and is maintained by Git.\n"
23702412
"# Any edits made in this file might be replaced in the future\n"
23712413
"# by a Git command.\n"
@@ -2374,12 +2416,12 @@ static int systemd_timer_write_unit_templates(const char *exec_path)
23742416
"Description=Optimize Git repositories data\n"
23752417
"\n"
23762418
"[Timer]\n"
2377-
"OnCalendar=%i\n"
2419+
"OnCalendar=%s\n"
23782420
"Persistent=true\n"
23792421
"\n"
23802422
"[Install]\n"
23812423
"WantedBy=timers.target\n";
2382-
if (fputs(unit, file) == EOF) {
2424+
if (fprintf(file, unit, schedule_pattern) < 0) {
23832425
error(_("failed to write to '%s'"), filename);
23842426
fclose(file);
23852427
goto error;
@@ -2388,9 +2430,36 @@ static int systemd_timer_write_unit_templates(const char *exec_path)
23882430
error_errno(_("failed to flush '%s'"), filename);
23892431
goto error;
23902432
}
2433+
2434+
res = 0;
2435+
2436+
error:
2437+
free(schedule_pattern);
2438+
free(local_timer_name);
23912439
free(filename);
2440+
return res;
2441+
}
23922442

2393-
filename = xdg_config_home_systemd("[email protected]");
2443+
/*
2444+
* No matter the schedule, we use the same service and can make use of the
2445+
* templating system. When installing git-maintenance@<schedule>.timer,
2446+
* systemd will notice that [email protected] exists as a template
2447+
* and will use this file and insert the <schedule> into the template at
2448+
* the position of "%i".
2449+
*/
2450+
static int systemd_timer_write_service_template(const char *exec_path)
2451+
{
2452+
int res = -1;
2453+
char *filename;
2454+
FILE *file;
2455+
const char *unit;
2456+
char *local_service_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "service");
2457+
2458+
filename = xdg_config_home_systemd(local_service_name);
2459+
if (safe_create_leading_directories(filename)) {
2460+
error(_("failed to create directories for '%s'"), filename);
2461+
goto error;
2462+
}
23942463
file = fopen_or_warn(filename, "w");
23952464
if (!file)
23962465
goto error;
@@ -2423,17 +2492,18 @@ static int systemd_timer_write_unit_templates(const char *exec_path)
24232492
error_errno(_("failed to flush '%s'"), filename);
24242493
goto error;
24252494
}
2426-
free(filename);
2427-
return 0;
2495+
2496+
res = 0;
24282497

24292498
error:
2499+
free(local_service_name);
24302500
free(filename);
2431-
systemd_timer_delete_unit_templates();
2432-
return -1;
2501+
return res;
24332502
}
24342503

24352504
static int systemd_timer_enable_unit(int enable,
2436-
enum schedule_priority schedule)
2505+
enum schedule_priority schedule,
2506+
int minute)
24372507
{
24382508
const char *cmd = "systemctl";
24392509
struct child_process child = CHILD_PROCESS_INIT;
@@ -2450,12 +2520,14 @@ static int systemd_timer_enable_unit(int enable,
24502520
*/
24512521
if (!enable)
24522522
child.no_stderr = 1;
2523+
else if (systemd_timer_write_timer_file(schedule, minute))
2524+
return -1;
24532525

24542526
get_schedule_cmd(&cmd, NULL);
24552527
strvec_split(&child.args, cmd);
24562528
strvec_pushl(&child.args, "--user", enable ? "enable" : "disable",
24572529
"--now", NULL);
2458-
strvec_pushf(&child.args, "git-maintenance@%s.timer", frequency);
2530+
strvec_pushf(&child.args, SYSTEMD_UNIT_FORMAT, frequency, "timer");
24592531

24602532
if (start_command(&child))
24612533
return error(_("failed to start systemctl"));
@@ -2472,24 +2544,58 @@ static int systemd_timer_enable_unit(int enable,
24722544
return 0;
24732545
}
24742546

2547+
/*
2548+
* A previous version of Git wrote the timer units as template files.
2549+
* Clean these up, if they exist.
2550+
*/
2551+
static void systemd_timer_delete_stale_timer_templates(void)
2552+
{
2553+
char *timer_template_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "timer");
2554+
char *filename = xdg_config_home_systemd(timer_template_name);
2555+
2556+
if (unlink(filename) && !is_missing_file_error(errno))
2557+
warning(_("failed to delete '%s'"), filename);
2558+
2559+
free(filename);
2560+
free(timer_template_name);
2561+
}
2562+
2563+
static int systemd_timer_delete_unit_files(void)
2564+
{
2565+
systemd_timer_delete_stale_timer_templates();
2566+
2567+
/* Purposefully not short-circuited to make sure all are called. */
2568+
return systemd_timer_delete_timer_file(SCHEDULE_HOURLY) |
2569+
systemd_timer_delete_timer_file(SCHEDULE_DAILY) |
2570+
systemd_timer_delete_timer_file(SCHEDULE_WEEKLY) |
2571+
systemd_timer_delete_service_template();
2572+
}
2573+
24752574
static int systemd_timer_delete_units(void)
24762575
{
2477-
return systemd_timer_enable_unit(0, SCHEDULE_HOURLY) ||
2478-
systemd_timer_enable_unit(0, SCHEDULE_DAILY) ||
2479-
systemd_timer_enable_unit(0, SCHEDULE_WEEKLY) ||
2480-
systemd_timer_delete_unit_templates();
2576+
int minute = get_random_minute();
2577+
/* Purposefully not short-circuited to make sure all are called. */
2578+
return systemd_timer_enable_unit(0, SCHEDULE_HOURLY, minute) |
2579+
systemd_timer_enable_unit(0, SCHEDULE_DAILY, minute) |
2580+
systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, minute) |
2581+
systemd_timer_delete_unit_files();
24812582
}
24822583

24832584
static int systemd_timer_setup_units(void)
24842585
{
2586+
int minute = get_random_minute();
24852587
const char *exec_path = git_exec_path();
24862588

2487-
int ret = systemd_timer_write_unit_templates(exec_path) ||
2488-
systemd_timer_enable_unit(1, SCHEDULE_HOURLY) ||
2489-
systemd_timer_enable_unit(1, SCHEDULE_DAILY) ||
2490-
systemd_timer_enable_unit(1, SCHEDULE_WEEKLY);
2589+
int ret = systemd_timer_write_service_template(exec_path) ||
2590+
systemd_timer_enable_unit(1, SCHEDULE_HOURLY, minute) ||
2591+
systemd_timer_enable_unit(1, SCHEDULE_DAILY, minute) ||
2592+
systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, minute);
2593+
24912594
if (ret)
24922595
systemd_timer_delete_units();
2596+
else
2597+
systemd_timer_delete_stale_timer_templates();
2598+
24932599
return ret;
24942600
}
24952601

t/t7900-maintenance.sh

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,15 @@ test_expect_success 'start and stop Linux/systemd maintenance' '
761761
# start registers the repo
762762
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
763763
764-
test_systemd_analyze_verify "systemd/user/[email protected]" &&
764+
for schedule in hourly daily weekly
765+
do
766+
test_path_is_file "systemd/user/git-maintenance@$schedule.timer" || return 1
767+
done &&
768+
test_path_is_file "systemd/user/[email protected]" &&
769+
770+
test_systemd_analyze_verify "systemd/user/[email protected]" &&
771+
test_systemd_analyze_verify "systemd/user/[email protected]" &&
772+
test_systemd_analyze_verify "systemd/user/[email protected]" &&
765773
766774
printf -- "--user enable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&
767775
test_cmp expect args &&
@@ -772,7 +780,10 @@ test_expect_success 'start and stop Linux/systemd maintenance' '
772780
# stop does not unregister the repo
773781
git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
774782
775-
test_path_is_missing "systemd/user/[email protected]" &&
783+
for schedule in hourly daily weekly
784+
do
785+
test_path_is_missing "systemd/user/git-maintenance@$schedule.timer" || return 1
786+
done &&
776787
test_path_is_missing "systemd/user/[email protected]" &&
777788
778789
printf -- "--user disable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&

0 commit comments

Comments
 (0)