Skip to content

Commit ce7fe66

Browse files
committed
Add cgroupV2 CPUQuotaPeriodUSec support
Signed-off-by: Austin Vazquez <[email protected]>
1 parent 32dca23 commit ce7fe66

File tree

4 files changed

+234
-29
lines changed

4 files changed

+234
-29
lines changed

cgroup2/cpu.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,26 @@ import (
2424

2525
type CPUMax string
2626

27+
const (
28+
// Default kernel value for cpu quota period is 100000 us (100 ms), same for v1 and v2.
29+
// v1: https://www.kernel.org/doc/html/latest/scheduler/sched-bwc.html and
30+
// v2: https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html
31+
defaultCPUMax = "max"
32+
defaultCPUMaxPeriod = 100000
33+
defaultCPUMaxPeriodStr = "100000"
34+
)
35+
2736
func NewCPUMax(quota *int64, period *uint64) CPUMax {
28-
max := "max"
37+
max := defaultCPUMax
2938
if quota != nil {
3039
max = strconv.FormatInt(*quota, 10)
3140
}
32-
return CPUMax(strings.Join([]string{max, strconv.FormatUint(*period, 10)}, " "))
41+
42+
duration := defaultCPUMaxPeriodStr
43+
if period != nil {
44+
duration = strconv.FormatUint(*period, 10)
45+
}
46+
return CPUMax(strings.Join([]string{max, duration}, " "))
3347
}
3448

3549
type CPU struct {
@@ -39,19 +53,34 @@ type CPU struct {
3953
Mems string
4054
}
4155

42-
func (c CPUMax) extractQuotaAndPeriod() (int64, uint64) {
56+
func (c CPUMax) extractQuotaAndPeriod() (int64, uint64, error) {
4357
var (
44-
quota int64
45-
period uint64
58+
quota int64 = math.MaxInt64
59+
period uint64 = defaultCPUMaxPeriod
60+
err error
4661
)
62+
63+
// value: quota [period]
4764
values := strings.Split(string(c), " ")
48-
if values[0] == "max" {
49-
quota = math.MaxInt64
50-
} else {
51-
quota, _ = strconv.ParseInt(values[0], 10, 64)
65+
if len(values) < 1 || len(values) > 2 {
66+
return 0, 0, ErrInvalidFormat
5267
}
53-
period, _ = strconv.ParseUint(values[1], 10, 64)
54-
return quota, period
68+
69+
if values[0] != defaultCPUMax {
70+
quota, err = strconv.ParseInt(values[0], 10, 64)
71+
if err != nil {
72+
return 0, 0, err
73+
}
74+
}
75+
76+
if len(values) == 2 {
77+
period, err = strconv.ParseUint(values[1], 10, 64)
78+
if err != nil {
79+
return 0, 0, err
80+
}
81+
}
82+
83+
return quota, period, nil
5584
}
5685

5786
func (r *CPU) Values() (o []Value) {

cgroup2/cpuv2_test.go

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,64 @@ func TestSystemdCgroupCpuController_NilWeight(t *testing.T) {
8383
}
8484

8585
func TestExtractQuotaAndPeriod(t *testing.T) {
86-
var (
87-
period uint64
88-
quota int64
86+
const (
87+
defaultQuota int64 = math.MaxInt64
88+
defaultPeriod uint64 = 100000
8989
)
90-
quota = 10000
91-
period = 8000
92-
cpuMax := NewCPUMax(&quota, &period)
93-
tquota, tPeriod := cpuMax.extractQuotaAndPeriod()
94-
95-
assert.Equal(t, quota, tquota)
96-
assert.Equal(t, period, tPeriod)
97-
98-
// case with nil quota which makes it "max" - max int val
99-
cpuMax2 := NewCPUMax(nil, &period)
100-
tquota2, tPeriod2 := cpuMax2.extractQuotaAndPeriod()
10190

102-
assert.Equal(t, int64(math.MaxInt64), tquota2)
103-
assert.Equal(t, period, tPeriod2)
91+
require.Equal(t, defaultCPUMaxPeriodStr, strconv.Itoa(defaultCPUMaxPeriod), "Constant for default period does not match its string type constant.")
92+
93+
// Default "max 100000"
94+
cpuMax := NewCPUMax(nil, nil)
95+
assert.Equal(t, CPUMax("max 100000"), cpuMax)
96+
quota, period, err := cpuMax.extractQuotaAndPeriod()
97+
assert.NoError(t, err)
98+
assert.Equal(t, defaultQuota, quota)
99+
assert.Equal(t, defaultPeriod, period)
100+
101+
// Only specifing limit is valid.
102+
cpuMax = CPUMax("max")
103+
quota, period, err = cpuMax.extractQuotaAndPeriod()
104+
assert.NoError(t, err)
105+
assert.Equal(t, defaultQuota, quota)
106+
assert.Equal(t, defaultPeriod, period)
107+
108+
tests := []struct {
109+
cpuMax string
110+
quota int64
111+
period uint64
112+
}{
113+
{
114+
cpuMax: "0 0",
115+
quota: 0,
116+
period: 0,
117+
},
118+
{
119+
cpuMax: "10000 8000",
120+
quota: 10000,
121+
period: 8000,
122+
},
123+
{
124+
cpuMax: "42000 4200",
125+
quota: 42000,
126+
period: 4200,
127+
},
128+
{
129+
cpuMax: "9223372036854775807 18446744073709551615",
130+
quota: 9223372036854775807,
131+
period: 18446744073709551615,
132+
},
133+
}
134+
135+
for _, test := range tests {
136+
t.Run(test.cpuMax, func(t *testing.T) {
137+
cpuMax := NewCPUMax(&test.quota, &test.period)
138+
assert.Equal(t, CPUMax(test.cpuMax), cpuMax)
139+
140+
tquota, tPeriod, err := cpuMax.extractQuotaAndPeriod()
141+
assert.NoError(t, err)
142+
assert.Equal(t, test.quota, tquota)
143+
assert.Equal(t, test.period, tPeriod)
144+
})
145+
}
104146
}

cgroup2/manager.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"path/filepath"
2828
"strconv"
2929
"strings"
30+
"sync"
3031
"time"
3132

3233
"github.com/containerd/cgroups/v3/cgroup2/stats"
@@ -47,7 +48,12 @@ const (
4748
defaultSlice = "system.slice"
4849
)
4950

50-
var canDelegate bool
51+
var (
52+
canDelegate bool
53+
54+
versionOnce sync.Once
55+
version int
56+
)
5157

5258
type Event struct {
5359
Low uint64
@@ -875,7 +881,21 @@ func NewSystemd(slice, group string, pid int, resources *Resources) (*Manager, e
875881
}
876882

877883
if resources.CPU != nil && resources.CPU.Max != "" {
878-
quota, period := resources.CPU.Max.extractQuotaAndPeriod()
884+
quota, period, err := resources.CPU.Max.extractQuotaAndPeriod()
885+
if err != nil {
886+
return &Manager{}, err
887+
}
888+
889+
if period != 0 {
890+
// systemd only supports CPUQuotaPeriodUSec since v242
891+
if sdVer := systemdVersion(conn); sdVer >= 242 {
892+
properties = append(properties, newSystemdProperty("CPUQuotaPeriodUSec", period))
893+
} else {
894+
log.G(context.TODO()).Debugf("systemd v%d is too old to support CPUQuotaPeriodSec "+
895+
" (setting will still be applied to cgroupfs)", sdVer)
896+
}
897+
}
898+
879899
// cpu.cfs_quota_us and cpu.cfs_period_us are controlled by systemd.
880900
// corresponds to USEC_INFINITY in systemd
881901
// if USEC_INFINITY is provided, CPUQuota is left unbound by systemd
@@ -915,6 +935,41 @@ func NewSystemd(slice, group string, pid int, resources *Resources) (*Manager, e
915935
}, nil
916936
}
917937

938+
// Adapted from https://github.com/opencontainers/cgroups/blob/9657f5a18b8d60a0f39fbb34d0cb7771e28e6278/systemd/common.go#L245-L281
939+
func systemdVersion(conn *systemdDbus.Conn) int {
940+
versionOnce.Do(func() {
941+
version = -1
942+
verStr, err := conn.GetManagerProperty("Version")
943+
if err == nil {
944+
version, err = systemdVersionAtoi(verStr)
945+
}
946+
947+
if err != nil {
948+
log.G(context.TODO()).WithError(err).Error("Unable to get systemd version")
949+
}
950+
})
951+
952+
return version
953+
}
954+
955+
func systemdVersionAtoi(str string) (int, error) {
956+
// Unconditionally remove the leading prefix ("v).
957+
str = strings.TrimLeft(str, `"v`)
958+
// Match on the first integer we can grab.
959+
for i := range len(str) {
960+
if str[i] < '0' || str[i] > '9' {
961+
// First non-digit: cut the tail.
962+
str = str[:i]
963+
break
964+
}
965+
}
966+
ver, err := strconv.Atoi(str)
967+
if err != nil {
968+
return -1, fmt.Errorf("can't parse version: %w", err)
969+
}
970+
return ver, nil
971+
}
972+
918973
func startUnit(conn *systemdDbus.Conn, group string, properties []systemdDbus.Property, ignoreExists bool) error {
919974
ctx := context.TODO()
920975

cgroup2/manager_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
package cgroup2
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"os"
2223
"os/exec"
2324
"syscall"
2425
"testing"
2526
"time"
2627

28+
systemdDbus "github.com/coreos/go-systemd/v22/dbus"
2729
"github.com/opencontainers/runtime-spec/specs-go"
2830
"github.com/stretchr/testify/assert"
2931
"github.com/stretchr/testify/require"
@@ -343,6 +345,83 @@ func TestSystemdCgroupPSIController(t *testing.T) {
343345
}
344346
}
345347

348+
func TestCPUQuotaPeriodUSec(t *testing.T) {
349+
checkCgroupMode(t)
350+
351+
// Create a unique group name using the current process ID
352+
group := fmt.Sprintf("testing-cpu-period-%d.scope", os.Getpid())
353+
354+
tests := []struct {
355+
name string
356+
quota int64
357+
period uint64
358+
}{
359+
{
360+
name: "quota 100000, default period",
361+
quota: 100000,
362+
},
363+
{
364+
name: "quota 10000, period 8000",
365+
quota: 10000,
366+
period: 8000,
367+
},
368+
}
369+
370+
for _, test := range tests {
371+
t.Run(test.name, func(t *testing.T) {
372+
var (
373+
cpuMax CPUMax
374+
expectedCPUMax string
375+
)
376+
if test.period == 0 {
377+
cpuMax = NewCPUMax(&test.quota, nil)
378+
expectedCPUMax = fmt.Sprintf("%d", test.quota)
379+
} else {
380+
cpuMax = NewCPUMax(&test.quota, &test.period)
381+
expectedCPUMax = fmt.Sprintf("%d %d", test.quota, test.period)
382+
}
383+
384+
c, err := NewSystemd("", group, os.Getpid(), &Resources{
385+
CPU: &CPU{
386+
Max: cpuMax,
387+
},
388+
})
389+
require.NoError(t, err, "failed to init new cgroup systemd manager")
390+
391+
checkFileContent(t, c.path, "cpu.max", expectedCPUMax)
392+
393+
conn, err := systemdDbus.NewWithContext(context.TODO())
394+
require.NoError(t, err, "failed to connect to systemd")
395+
defer conn.Close()
396+
397+
sdVer := systemdVersion(conn)
398+
if sdVer >= 242 {
399+
unitName := systemdUnitFromPath(c.path)
400+
props, err := conn.GetAllPropertiesContext(context.TODO(), unitName)
401+
require.NoError(t, err, "failed to get unit properties")
402+
403+
periodUSec, ok := props["CPUQuotaPeriodUSec"]
404+
require.True(t, ok, "CPUQuotaPeriodUSec property not found")
405+
require.Equal(t, test.period, periodUSec.(uint64), "CPUQuotaPeriodUSec value doesn't match expected period")
406+
} else {
407+
t.Logf("Skipping CPUQuotaPeriodUSec test for systemd version %d", sdVer)
408+
}
409+
})
410+
}
411+
412+
// // Also verify CPUQuotaPerSecUSec is set correctly
413+
// quotaPerSecUSec, ok := props["CPUQuotaPerSecUSec"]
414+
// require.True(t, ok, "CPUQuotaPerSecUSec property not found")
415+
416+
// // Calculate expected value: quota*1000000/period
417+
// expectedQuotaPerSecUSec := uint64(quota*1000000) / period
418+
// // Round up to nearest 10000 if needed
419+
// if expectedQuotaPerSecUSec%10000 != 0 {
420+
// expectedQuotaPerSecUSec = ((expectedQuotaPerSecUSec / 10000) + 1) * 10000
421+
// }
422+
// require.Equal(t, expectedQuotaPerSecUSec, quotaPerSecUSec.(uint64), "CPUQuotaPerSecUSec value doesn't match expected value")
423+
}
424+
346425
func BenchmarkStat(b *testing.B) {
347426
checkCgroupMode(b)
348427
group := "/stat-test-cg"

0 commit comments

Comments
 (0)