Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions systemd/systemd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package systemd

import (
"os"
"os/exec"
"reflect"
"strconv"
"testing"

systemdDbus "github.com/coreos/go-systemd/v22/dbus"
Expand Down Expand Up @@ -157,6 +159,12 @@ func TestUnifiedResToSystemdProps(t *testing.T) {
newProp("CPUWeight", uint64(1000)),
},
},
{
name: "memory.oom.group handled by Apply method",
res: map[string]string{
"memory.oom.group": "1",
},
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -272,3 +280,116 @@ func TestTasksMax(t *testing.T) {
t.Fatalf("failed to set PidsLimit=nil: %v", err)
}
}

func TestOOMPolicyApply(t *testing.T) {
if !IsRunningSystemd() {
t.Skip("Test requires systemd.")
}
if !cgroups.IsCgroup2UnifiedMode() {
t.Skip("cgroup v2 is required")
}
if os.Geteuid() != 0 {
t.Skip("Test requires root.")
}
Comment thread
kolyshkin marked this conversation as resolved.

cm := newDbusConnManager(false)
if systemdVersion(cm) < oomPolicySupportedVersion {
t.Skipf("Test requires systemd >= %d (OOMPolicy on scopes)", oomPolicySupportedVersion)
}

testCases := []struct {
name string
oomGroupValue string
expectedPolicy string
expectError bool
}{
{
name: "memory.oom.group=0 sets OOMPolicy=continue",
oomGroupValue: "0",
expectedPolicy: "continue",
expectError: false,
},
{
name: "memory.oom.group=1 sets OOMPolicy=kill",
oomGroupValue: "1",
expectedPolicy: "kill",
expectError: false,
},
{
name: "invalid memory.oom.group value",
oomGroupValue: "invalid",
expectError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := &cgroups.Cgroup{
ScopePrefix: "test",
Name: "test-oom-policy-" + strconv.FormatInt(int64(os.Getpid()), 10),
Resources: &cgroups.Resources{
Unified: map[string]string{
"memory.oom.group": tc.oomGroupValue,
},
},
}

manager, err := NewUnifiedManager(config, "")
if err != nil {
t.Fatalf("Failed to create manager: %v", err)
}
defer func() {
_ = manager.Destroy()
}()

// Scopes require a process inside.
cmd := exec.Command("bash", "-c", "sleep 1m")
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
// Make sure to not leave a zombie.
defer func() {
// These may fail, we don't care.
_ = cmd.Process.Kill()
_ = cmd.Wait()
}()

err = manager.Apply(cmd.Process.Pid)
if tc.expectError {
if err == nil {
t.Fatal("Expected error but got none")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

unitName := getUnitName(config)
conn, err := systemdDbus.NewSystemdConnectionContext(t.Context())
if err != nil {
t.Fatalf("Failed to connect to systemd: %v", err)
}
defer conn.Close()

properties, err := conn.GetUnitTypePropertiesContext(t.Context(), unitName, "Scope")
if err != nil {
t.Fatalf("Failed to get unit type properties: %v", err)
}

oomPolicyValue, exists := properties["OOMPolicy"]
if !exists {
t.Fatal("OOMPolicy property not found")
}

oomPolicyStr, ok := oomPolicyValue.(string)
if !ok {
t.Fatalf("OOMPolicy value is not a string: %T", oomPolicyValue)
}

if oomPolicyStr != tc.expectedPolicy {
t.Errorf("Expected OOMPolicy=%s, got %s", tc.expectedPolicy, oomPolicyStr)
}
})
}
}
35 changes: 27 additions & 8 deletions systemd/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import (
)

const (
cpuIdleSupportedVersion = 252
cpuIdleSupportedVersion = 252
oomPolicySupportedVersion = 253
)

type UnifiedManager struct {
Expand Down Expand Up @@ -183,13 +184,8 @@ func unifiedResToSystemdProps(cm *dbusConnManager, res map[string]string) (props
newProp("TasksMax", num))

case "memory.oom.group":
// Setting this to 1 is roughly equivalent to OOMPolicy=kill
// (as per systemd.service(5) and
// https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html),
// but it's not clear what to do if it is unset or set
// to 0 in runc update, as there are two other possible
// values for OOMPolicy (continue/stop).
fallthrough
// This was set before the unit started, so no need to

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dumb question, do we know this to actually be true?

i.e. we've made it so we also process memory.oom.group in Apply(), which iiuc is getting its config for this from when you setup a new manager, but for Set() we're sort of hoping the user already set it up initially and just avoid warning on it. The downside here then is if someone doesn't have it configured for Apply(), and then adds it later with Set(), systemd won't be in the loop and a daemon-reload will cause systemd to overwrite with whatever its OOMPolicy is set to be for the unit?

I guess I don't have a better answer for these sort of "must be done on unit creation" type settings unless we can warn iff we know the systemd prop isn't aligned already.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but for Set() we're sort of hoping the user already set it up initially and just avoid warning on it

Oh, I suppose what we should do is query the existing value, and warn/error if they mismatch? because you're right: if someone does a runc update with a new value for this, we cannot actually set it.

// warn about it here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the warning should be louder (maybe just more explicitly stating systemd might override this on a daemon-reload) for other uses that hit it since systemd will stomp on it as you highlight, or is that only for a subset of things that systemd has a knob for? I realize that's a bit out of scope for this PR, but sort of a tricky thing for folks to debug down to.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for memory.oom.group specifically, I think we can (have to?) assume that it was set up in Apply() viz. this patch (modulo bugs). But r.e. your comment, I wonder if we should make the logging in the default case at least a warning level, or maybe explicitly generate an error?


default:
// Ignore the unknown resource here -- will still be
Expand Down Expand Up @@ -338,6 +334,29 @@ func (m *UnifiedManager) Apply(pid int) error {

properties = append(properties, c.SystemdProps...)

// OOMPolicy must be set at unit creation time, systemd does not allow
// changing it on a running scope for some reason despite the kernel allowing so
// so we do this here in Apply() instead of Set()
if c.Resources != nil {
if v, ok := c.Resources.Unified["memory.oom.group"]; ok {
if systemdVersion(m.dbus) >= oomPolicySupportedVersion {
value, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return fmt.Errorf("unified resource %q value conversion error: %w", "memory.oom.group", err)
}

switch value {
case 0:
properties = append(properties, newProp("OOMPolicy", "continue"))
case 1:
properties = append(properties, newProp("OOMPolicy", "kill"))
default:
logrus.Debugf("don't know how to convert memory.oom.group=%d; skipping (will still be applied to cgroupfs)", value)
}
}
}
}

if err := startUnit(m.dbus, unitName, properties, pid == -1); err != nil {
return fmt.Errorf("unable to start unit %q (properties %+v): %w", unitName, properties, err)
}
Expand Down
Loading