Skip to content
Open
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
27 changes: 22 additions & 5 deletions systemd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const (
// v1: https://www.kernel.org/doc/html/latest/scheduler/sched-bwc.html and
// v2: https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html
defCPUQuotaPeriod = uint64(100000)

// maxCPUQuota is the maximum CPU quota (in microseconds) accepted by the
// kernel (see max_cfs_runtime in kernel/sched/core.c and MAX_BW in
// kernel/sched/sched.h). A larger quota is treated as unlimited.
// Note that maxCPUQuota * 1000000 does not overflow uint64.
maxCPUQuota = 1<<44 - 1
)

var (
Expand Down Expand Up @@ -320,11 +326,22 @@ func addCPUQuota(cm *dbusConnManager, properties *[]systemdDbus.Property, quota
// (integer percentage of CPU) internally. This means that if a fractional percent of
// CPU is indicated by Resources.CpuQuota, we need to round up to the nearest
// 10ms (1% of a second) such that child cgroups can set the cpu.cfs_quota_us they expect.
cpuQuotaPerSecUSec = uint64(*quota*1000000) / period
if cpuQuotaPerSecUSec%10000 != 0 {
cpuQuotaPerSecUSec = ((cpuQuotaPerSecUSec / 10000) + 1) * 10000
// Update the requested quota along with the round-up in order to write the same value to cgroupfs.
*quota = int64(cpuQuotaPerSecUSec) * int64(period) / 1000000
if *quota <= maxCPUQuota {
// No overflow is possible here since *quota <= maxCPUQuota.
cpuQuotaPerSecUSec = uint64(*quota) * 1000000 / period
if cpuQuotaPerSecUSec%10000 != 0 {
cpuQuotaPerSecUSec = ((cpuQuotaPerSecUSec / 10000) + 1) * 10000
// Update the requested quota along with the round-up in order to write the same value to cgroupfs.
// This is cpuQuotaPerSecUSec * period / 1000000, rearranged to avoid overflow.
*quota = int64(cpuQuotaPerSecUSec / 10000 * period / 100)
}
}
if *quota > maxCPUQuota {
// The quota (possibly after the round-up above) is too large
// for the kernel to accept. Since it is effectively unlimited,
// treat it as such, for both systemd and cgroupfs.
cpuQuotaPerSecUSec = math.MaxUint64
*quota = -1
}
}
*properties = append(*properties,
Expand Down
105 changes: 105 additions & 0 deletions systemd/systemd_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package systemd

import (
"maps"
"math"
"os"
"os/exec"
"reflect"
Expand Down Expand Up @@ -115,6 +117,7 @@ func TestUnifiedResToSystemdProps(t *testing.T) {
res map[string]string
expError bool
expProps []systemdDbus.Property
resAfter map[string]string // If nil, res is expected to be unchanged.
}{
{
name: "empty map",
Expand Down Expand Up @@ -233,13 +236,81 @@ func TestUnifiedResToSystemdProps(t *testing.T) {
},
expError: true,
},
{
name: "cpu.max",
minVer: cpuQuotaPeriodSupportedVersion,
res: map[string]string{
"cpu.max": "500000 1000000",
},
expProps: []systemdDbus.Property{
newProp("CPUQuotaPeriodUSec", uint64(1000000)),
newProp("CPUQuotaPerSecUSec", uint64(500000)),
},
},
{
name: "cpu.max with round up",
minVer: cpuQuotaPeriodSupportedVersion,
res: map[string]string{
"cpu.max": "123456 100000",
},
expProps: []systemdDbus.Property{
newProp("CPUQuotaPeriodUSec", uint64(100000)),
newProp("CPUQuotaPerSecUSec", uint64(1240000)),
},
resAfter: map[string]string{
"cpu.max": "124000 100000",
},
},
{
name: "cpu.max with round up, no period",
minVer: cpuQuotaPeriodSupportedVersion,
res: map[string]string{
"cpu.max": "123456",
},
expProps: []systemdDbus.Property{
newProp("CPUQuotaPeriodUSec", uint64(100000)),
newProp("CPUQuotaPerSecUSec", uint64(1240000)),
},
resAfter: map[string]string{
"cpu.max": "124000",
},
},
{
name: "cpu.max too large",
minVer: cpuQuotaPeriodSupportedVersion,
res: map[string]string{
"cpu.max": "9223372036854700 100000",
},
expProps: []systemdDbus.Property{
newProp("CPUQuotaPeriodUSec", uint64(100000)),
newProp("CPUQuotaPerSecUSec", uint64(math.MaxUint64)),
},
resAfter: map[string]string{
"cpu.max": "max 100000",
},
},
{
name: "cpu.max=max",
minVer: cpuQuotaPeriodSupportedVersion,
res: map[string]string{
"cpu.max": "max 100000",
},
expProps: []systemdDbus.Property{
newProp("CPUQuotaPeriodUSec", uint64(100000)),
newProp("CPUQuotaPerSecUSec", uint64(math.MaxUint64)),
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.minVer != 0 && systemdVersion(cm) < tc.minVer {
t.Skipf("requires systemd >= %d", tc.minVer)
}
resAfter := tc.resAfter
if resAfter == nil {
resAfter = maps.Clone(tc.res)
}
props, err := unifiedResToSystemdProps(cm, tc.res)
if err != nil && !tc.expError {
t.Fatalf("expected no error, got: %v", err)
Expand All @@ -250,6 +321,9 @@ func TestUnifiedResToSystemdProps(t *testing.T) {
if !reflect.DeepEqual(tc.expProps, props) {
t.Errorf("wrong properties (exp %+v, got %+v)", tc.expProps, props)
}
if !maps.Equal(resAfter, tc.res) {
t.Errorf("wrong resources (exp %+v, got %+v)", resAfter, tc.res)
}
})
}
}
Expand Down Expand Up @@ -288,6 +362,37 @@ func TestAddCPUQuota(t *testing.T) {
expectedCPUQuotaPerSecUSec: 560000,
expectedQuota: 504000,
},
{
name: "Large quota with round up",
quota: 10000000000001,
expectedCPUQuotaPerSecUSec: 100000000010000,
expectedQuota: 10000000001000,
},
{
name: "Max quota",
quota: maxCPUQuota,
period: 1500,
expectedCPUQuotaPerSecUSec: 11728124029610000,
expectedQuota: maxCPUQuota,
},
{
name: "Max quota, round up above max",
quota: maxCPUQuota,
expectedCPUQuotaPerSecUSec: math.MaxUint64,
expectedQuota: -1,
},
{
name: "Quota too large",
quota: maxCPUQuota + 1,
expectedCPUQuotaPerSecUSec: math.MaxUint64,
expectedQuota: -1,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps silly but should we define a const for the magic -1 so that it's clear we're talking about "unlimited" (not an accidental negative)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd keep this as is -- in cgroupfs, -1 always means "unlimited" and the value is used in this repo quite a lot of times. If we want to change it, let's change it everywhere (and not in this PR).

},
{
name: "Max int64 quota",
quota: math.MaxInt64,
expectedCPUQuotaPerSecUSec: math.MaxUint64,
expectedQuota: -1,
},
}

for _, tc := range testCases {
Expand Down
16 changes: 15 additions & 1 deletion systemd/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"errors"
"fmt"
"maps"
"math"
"os"
"path/filepath"
Expand Down Expand Up @@ -76,6 +77,8 @@ func shouldSetCPUIdle(cm *dbusConnManager, v string) bool {
// For the list of keys, see https://www.kernel.org/doc/Documentation/cgroup-v2.txt
//
// For the list of systemd unit properties, see systemd.resource-control(5).
//
// The value of res["cpu.max"] may be modified (see addCPUQuota).
func unifiedResToSystemdProps(cm *dbusConnManager, res map[string]string) (props []systemdDbus.Property, _ error) {
var err error

Expand Down Expand Up @@ -121,7 +124,17 @@ func unifiedResToSystemdProps(cm *dbusConnManager, res map[string]string) (props
return nil, fmt.Errorf("unified resource %q quota value conversion error: %w", k, err)
}
}
origQuota := quota
addCPUQuota(cm, &props, &quota, period)
if quota != origQuota {
// Update the value along with the round-up in order
// to write the same value to cgroupfs.
sv[0] = "max"
if quota > 0 {
sv[0] = strconv.FormatInt(quota, 10)
}
res[k] = strings.Join(sv, " ")
}

case "cpu.weight":
if shouldSetCPUIdle(cm, strings.TrimSpace(res["cpu.idle"])) {
Expand Down Expand Up @@ -554,8 +567,9 @@ func (m *UnifiedManager) Set(r *cgroups.Resources) error {
if r == nil {
return nil
}
// Use a copy since CpuQuota in r may be modified.
// Use a copy since CpuQuota and Unified["cpu.max"] in r may be modified.
rCopy := *r
rCopy.Unified = maps.Clone(r.Unified)
r = &rCopy
properties, err := genV2ResourcesProperties(m.fsMgr.Path(""), r, m.dbus)
if err != nil {
Expand Down
Loading