Skip to content

Commit

Permalink
Setup systemd priority (#409)
Browse files Browse the repository at this point in the history
* add fields to schedule config

* take nice & ionice values from global

* add unit tests

* docs: improve IONice flags description

* docs: add section about systemd unit configuration
  • Loading branch information
creativeprojects authored Oct 5, 2024
1 parent da03de7 commit 830d0fd
Show file tree
Hide file tree
Showing 13 changed files with 328 additions and 79 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ status.json

go.work
go.work.sum

/*.txt
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,17 @@ fix:
GOOS=darwin golangci-lint run --fix
GOOS=linux golangci-lint run --fix
GOOS=windows golangci-lint run --fix

.PHONY: deploy-current
deploy-current: build-linux build-pi
@echo "[*] $@"
for server in $$(cat targets_amd64.txt); do \
echo "Deploying to $$server" ; \
rsync -avz --progress $(BINARY_LINUX_AMD64) $$server: ; \
ssh $$server "sudo -S install $(BINARY_LINUX_AMD64) /usr/local/bin/resticprofile" ; \
done
for server in $$(cat targets_armv6.txt); do \
echo "Deploying to $$server" ; \
rsync -avz --progress $(BINARY_PI) $$server: ; \
ssh $$server "sudo -S install $(BINARY_PI) /usr/local/bin/resticprofile" ; \
done
10 changes: 5 additions & 5 deletions config/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (

// Global holds the configuration from the global section
type Global struct {
IONice bool `mapstructure:"ionice" default:"false" description:"Enables setting the unix IO priority class and level for resticprofile and child processes (only on unix OS)."`
IONiceClass int `mapstructure:"ionice-class" default:"2" range:"[1:3]" description:"Sets the unix \"ionice-class\" to apply when \"ionice\" is enabled"`
IONiceLevel int `mapstructure:"ionice-level" default:"0" range:"[0:7]" description:"Sets the unix \"ionice-level\" to apply when \"ionice\" is enabled"`
IONice bool `mapstructure:"ionice" default:"false" description:"Enables setting the linux IO priority class and level for resticprofile and child processes (only on linux OS)."`
IONiceClass int `mapstructure:"ionice-class" default:"2" range:"[1:3]" description:"Sets the linux \"ionice-class\" (I/O scheduling class) to apply when \"ionice\" is enabled (1=realtime, 2=best-effort, 3=idle)"`
IONiceLevel int `mapstructure:"ionice-level" default:"0" range:"[0:7]" description:"Sets the linux \"ionice-level\" (I/O priority within the scheduling class) to apply when \"ionice\" is enabled (0=highest priority, 7=lowest priority)"`
Nice int `mapstructure:"nice" default:"0" range:"[-20:19]" description:"Sets the unix \"nice\" value for resticprofile and child processes (on any OS)"`
Priority string `mapstructure:"priority" default:"normal" enum:"idle;background;low;normal;high;highest" description:"Sets process priority class for resticprofile and child processes (on any OS)"`
DefaultCommand string `mapstructure:"default-command" default:"snapshots" description:"The restic or resticprofile command to use when no command was specified"`
Expand All @@ -20,8 +20,8 @@ type Global struct {
ResticBinary string `mapstructure:"restic-binary" description:"Full path of the restic executable (detected if not set)"`
ResticVersion string // not configurable at the moment. To be set after ResticBinary is known.
FilterResticFlags bool `mapstructure:"restic-arguments-filter" default:"true" description:"Remove unknown flags instead of passing all configured flags to restic"`
ResticLockRetryAfter time.Duration `mapstructure:"restic-lock-retry-after" default:"1m" description:"Time to wait before trying to get a lock on a restic repositoey - see https://creativeprojects.github.io/resticprofile/usage/locks/"`
ResticStaleLockAge time.Duration `mapstructure:"restic-stale-lock-age" default:"1h" description:"The age an unused lock on a restic repository must have at least before resiticprofile attempts to unlock - see https://creativeprojects.github.io/resticprofile/usage/locks/"`
ResticLockRetryAfter time.Duration `mapstructure:"restic-lock-retry-after" default:"1m" description:"Time to wait before trying to get a lock on a restic repository - see https://creativeprojects.github.io/resticprofile/usage/locks/"`
ResticStaleLockAge time.Duration `mapstructure:"restic-stale-lock-age" default:"1h" description:"The age an unused lock on a restic repository must have at least before resticprofile attempts to unlock - see https://creativeprojects.github.io/resticprofile/usage/locks/"`
ShellBinary []string `mapstructure:"shell" default:"auto" examples:"sh;bash;pwsh;powershell;cmd" description:"The shell that is used to run commands (default is OS specific)"`
MinMemory uint64 `mapstructure:"min-memory" default:"100" description:"Minimum available memory (in MB) required to run any commands - see https://creativeprojects.github.io/resticprofile/usage/memory/"`
Scheduler string `mapstructure:"scheduler" default:"auto" examples:"auto;launchd;systemd;taskscheduler;crond;crond:/usr/bin/crontab;crontab:*:/etc/cron.d/resticprofile" description:"Selects the scheduler. Blank or \"auto\" uses the default scheduler of your operating system: \"launchd\", \"systemd\", \"taskscheduler\" or \"crond\" (as fallback). Alternatively you can set \"crond\" for cron compatible schedulers supporting the crontab executable API or \"crontab:[user:]file\" to write into a crontab file directly. The need for a user is detected if missing and can be set to a name, \"-\" (no user) or \"*\" (current user)."`
Expand Down
2 changes: 1 addition & 1 deletion crond/crontab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ func TestUseCrontabBinary(t *testing.T) {
binary := platform.Executable("./crontab")
defer func() { _ = os.Remove(binary) }()

cmd := exec.Command("go", "build", "-o", binary, "./mock")
cmd := exec.Command("go", "build", "-buildvcs=false", "-o", binary, "./mock")
require.NoError(t, cmd.Run())

crontab := NewCrontab(nil)
Expand Down
2 changes: 1 addition & 1 deletion docs/content/configuration/priority.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ You can lower the priority of restic to avoid slowing down other processes. This

## Nice

You can use these values for the `priority` parameter:
You can use these values for the `priority` parameter, string or numeric values are both valid:

| String value | "nice" equivalent on unixes | Notes |
|--------------|-----------------------------|------|
Expand Down
29 changes: 26 additions & 3 deletions docs/content/schedules/systemd.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Systemd"
weight: 105
tags: ["v0.25.0"]
tags: ["v0.25.0", "v0.29.0"]
---


Expand Down Expand Up @@ -48,6 +48,7 @@ Specifying the profile option `schedule-after-network-online: true` means that t
for a network connection before running.
This is done via an [After=network-online.target](https://systemd.io/NETWORK_ONLINE/) entry in the service.


## systemd drop-in files

It is possible to automatically populate `*.conf.d`
Expand Down Expand Up @@ -158,6 +159,22 @@ pass = $(systemd-ask-password -n "smb restic user password" | rclone obscure -)
EOF
```

## Priority and CPU scheduling

resticprofile allows you to set the `nice` value, the CPU scheduling policy and IO nice values for the systemd service.
This is only working properly for resticprofile >= 0.29.0.

| systemd unit option | resticprofile option |
|----------------------|----------------------|
| CPUSchedulingPolicy | set to `idle` if schedule `priority` = `background` , otherwise default to standard policy |
| Nice | `nice` from `global` section |
| IOSchedulingClass | `ionice-class` from `global` section |
| IOSchedulingPriority | `ionice-level` from `global` section |

{{% notice note %}}
When setting the `CPUSchedulingPolicy` to `idle` (by setting `priority` to `background`), the backup might never execute if all your CPU cores are always busy.
{{% /notice %}}

## How to change the default systemd unit and timer file using a template

By default, an opinionated systemd unit and timer are automatically generated by resticprofile.
Expand Down Expand Up @@ -219,12 +236,18 @@ Here are the defaults if you don't specify your own (which I recommend to use as
Description={{ .JobDescription }}
{{ if .AfterNetworkOnline }}After=network-online.target
{{ end }}

[Service]
Type=notify
WorkingDirectory={{ .WorkingDirectory }}
ExecStart={{ .CommandLine }}
{{ if .Nice }}Nice={{ .Nice }}{{ end }}
{{ if .Nice }}Nice={{ .Nice }}
{{ end -}}
{{ if .CPUSchedulingPolicy }}CPUSchedulingPolicy={{ .CPUSchedulingPolicy }}
{{ end -}}
{{ if .IOSchedulingClass }}IOSchedulingClass={{ .IOSchedulingClass }}
{{ end -}}
{{ if .IOSchedulingPriority }}IOSchedulingPriority={{ .IOSchedulingPriority }}
{{ end -}}
{{ range .Environment -}}
Environment="{{ . }}"
{{ end -}}
Expand Down
8 changes: 6 additions & 2 deletions examples/linux.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ global:
priority: low
systemd-unit-template: sample.service
prevent-sleep: false
ionice: true
ionice-class: 3
ionice-level: 7
nice: 19

default:
password-file: key
Expand Down Expand Up @@ -57,12 +61,12 @@ test2:
backup:
source: ./
schedule: "*:05,20,35,50"
schedule-permission: system
schedule-permission: user
schedule-log: backup-test2.log
run-after: "chown -R $SUDO_USER $HOME/.cache/restic /tmp/backup"
check:
schedule: "*-*-2"
schedule-permission: system
schedule-permission: user
schedule-log: check-test2.log

test3:
Expand Down
9 changes: 8 additions & 1 deletion examples/sample.service
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ OnFailure=unit-status-mail@%n.service
Type=notify
WorkingDirectory={{ .WorkingDirectory }}
ExecStart={{ .CommandLine }}
{{ if .Nice }}Nice={{ .Nice }}{{ end }}
{{ if .Nice }}Nice={{ .Nice }}
{{ end -}}
{{ if .CPUSchedulingPolicy }}CPUSchedulingPolicy={{ .CPUSchedulingPolicy }}
{{ end -}}
{{ if .IOSchedulingClass }}IOSchedulingClass={{ .IOSchedulingClass }}
{{ end -}}
{{ if .IOSchedulingPriority }}IOSchedulingPriority={{ .IOSchedulingPriority }}
{{ end -}}
{{ range .Environment -}}
Environment="{{ . }}"
{{ end -}}
31 changes: 17 additions & 14 deletions schedule/handler_systemd.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,23 @@ func (h *HandlerSystemd) CreateJob(job *Config, schedules []*calendar.Event, per
}

err := systemd.Generate(systemd.Config{
CommandLine: job.Command + " --no-prio " + strings.Join(job.Arguments, " "),
Environment: job.Environment,
WorkingDirectory: job.WorkingDirectory,
Title: job.ProfileName,
SubTitle: job.CommandName,
JobDescription: job.JobDescription,
TimerDescription: job.TimerDescription,
Schedules: job.Schedules,
UnitType: unitType,
Priority: job.GetPriority(),
UnitFile: h.config.UnitTemplate,
TimerFile: h.config.TimerTemplate,
AfterNetworkOnline: job.AfterNetworkOnline,
DropInFiles: job.SystemdDropInFiles,
CommandLine: job.Command + " " + strings.Join(append([]string{"--no-prio"}, job.Arguments...), " "),
Environment: job.Environment,
WorkingDirectory: job.WorkingDirectory,
Title: job.ProfileName,
SubTitle: job.CommandName,
JobDescription: job.JobDescription,
TimerDescription: job.TimerDescription,
Schedules: job.Schedules,
UnitType: unitType,
Priority: job.GetPriority(),
UnitFile: h.config.UnitTemplate,
TimerFile: h.config.TimerTemplate,
AfterNetworkOnline: job.AfterNetworkOnline,
DropInFiles: job.SystemdDropInFiles,
Nice: h.config.Nice,
IOSchedulingClass: h.config.IONiceClass,
IOSchedulingPriority: h.config.IONiceLevel,
})
if err != nil {
return err
Expand Down
31 changes: 23 additions & 8 deletions schedule/scheduler_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ func (s SchedulerCrond) Convert(_ string) SchedulerConfig { return s }
type SchedulerSystemd struct {
UnitTemplate string
TimerTemplate string
Nice int
IONiceClass int
IONiceLevel int
}

func (s SchedulerSystemd) Type() string { return constants.SchedulerSystemd }
Expand All @@ -70,6 +73,7 @@ func NewSchedulerConfig(global *config.Global) SchedulerConfig {
} else {
return SchedulerCrond{}
}

case constants.SchedulerCrontab:
if len(resource) > 0 {
if user, location, found := strings.Cut(resource, ":"); found {
Expand All @@ -85,27 +89,38 @@ func NewSchedulerConfig(global *config.Global) SchedulerConfig {
} else {
panic(fmt.Errorf("invalid schedule %q, no crontab file was specified, expecting \"%s: filename\"", scheduler, scheduler))
}

case constants.SchedulerLaunchd:
return SchedulerLaunchd{}

case constants.SchedulerSystemd:
return SchedulerSystemd{
UnitTemplate: global.SystemdUnitTemplate,
TimerTemplate: global.SystemdTimerTemplate,
}
return getSchedulerSystemdDefaultConfig(global)

case constants.SchedulerWindows:
return SchedulerWindows{}

default:
return SchedulerDefaultOS{
defaults: []SchedulerConfig{
SchedulerSystemd{
UnitTemplate: global.SystemdUnitTemplate,
TimerTemplate: global.SystemdTimerTemplate,
},
getSchedulerSystemdDefaultConfig(global),
},
}
}
}

func getSchedulerSystemdDefaultConfig(global *config.Global) SchedulerSystemd {
scheduler := SchedulerSystemd{
UnitTemplate: global.SystemdUnitTemplate,
TimerTemplate: global.SystemdTimerTemplate,
Nice: global.Nice,
}
if global.IONice {
scheduler.IONiceClass = global.IONiceClass
scheduler.IONiceLevel = global.IONiceLevel
}
return scheduler
}

var (
_ SchedulerConfig = SchedulerDefaultOS{}
_ SchedulerConfig = SchedulerCrond{}
Expand Down
51 changes: 46 additions & 5 deletions schedule/scheduler_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,53 @@ func TestOsDefaultConfig(t *testing.T) {
}

func TestSystemdConfig(t *testing.T) {
g := &config.Global{
Scheduler: constants.SchedulerSystemd,
SystemdTimerTemplate: "timer.tpl",
SystemdUnitTemplate: "unit.tpl",
testCases := []struct {
global *config.Global
expected SchedulerSystemd
}{
{
global: &config.Global{
Scheduler: constants.SchedulerSystemd,
SystemdTimerTemplate: "timer.tpl",
SystemdUnitTemplate: "unit.tpl",
},
expected: SchedulerSystemd{TimerTemplate: "timer.tpl", UnitTemplate: "unit.tpl"},
},
{
global: &config.Global{
Scheduler: constants.SchedulerSystemd,
SystemdTimerTemplate: "timer.tpl",
SystemdUnitTemplate: "unit.tpl",
IONiceClass: 3,
IONiceLevel: 5,
},
expected: SchedulerSystemd{TimerTemplate: "timer.tpl", UnitTemplate: "unit.tpl"},
},
{
global: &config.Global{
Scheduler: constants.SchedulerSystemd,
SystemdTimerTemplate: "timer.tpl",
SystemdUnitTemplate: "unit.tpl",
IONice: true,
IONiceClass: 3,
IONiceLevel: 5,
},
expected: SchedulerSystemd{TimerTemplate: "timer.tpl", UnitTemplate: "unit.tpl", IONiceClass: 3, IONiceLevel: 5},
},
{
global: &config.Global{
Scheduler: constants.SchedulerSystemd,
SystemdTimerTemplate: "timer.tpl",
SystemdUnitTemplate: "unit.tpl",
Nice: 12,
},
expected: SchedulerSystemd{TimerTemplate: "timer.tpl", UnitTemplate: "unit.tpl", Nice: 12},
},
}

for _, tc := range testCases {
assert.Equal(t, tc.expected, NewSchedulerConfig(tc.global))
}
assert.Equal(t, SchedulerSystemd{TimerTemplate: "timer.tpl", UnitTemplate: "unit.tpl"}, NewSchedulerConfig(g))
}

func TestCrondConfig(t *testing.T) {
Expand Down
Loading

0 comments on commit 830d0fd

Please sign in to comment.