Skip to content

Commit 840ff55

Browse files
rogercollmx-psi
authored andcommitted
[chore] Add extension/cgroupruntime integration tests (open-telemetry#36617)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Adds some integration tests for the extension. It uses the `containerd/cgroups` package to modify the current process's allocated cgroup resources and assert the corresponding values for GOMEMLIMIT/GOMAXPROCS set by the extension. <!-- Issue number (e.g. open-telemetry#1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes open-telemetry#36545 <!--Describe what testing was performed and which tests were added.--> #### Testing Cgroup resources modification requires privileged access in GHA runner instances, thus the test must be run with `sudo`. The `go` toolchain has an `exec` flag to run tests binary(s) via another binary such as sudo. The Makefile has been modified to run Go tests files with build tag `integration` && `sudo` with the sudo command. I am not very confident with this solution, as I could not find any other component requiring privileged execution for its integration tests and the "go test -tags=integration,sudo" would run for all of them. I am all ears on other testing strategies for this use case. Similar strategy in cgroups package https://github.com/containerd/cgroups/blob/main/.github/workflows/ci.yml#L101 <!--Describe the documentation added.--> #### Documentation <!--Please delete paragraphs that you did not use before submitting.--> --------- Co-authored-by: Pablo Baeyens <[email protected]>
1 parent bec1ef2 commit 840ff55

File tree

4 files changed

+264
-22
lines changed

4 files changed

+264
-22
lines changed

Makefile.Common

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ GO_BUILD_TAGS=""
2929
GO_BUILD_LDFLAGS="-s -w"
3030
GOTEST_TIMEOUT?= 600s
3131
GOTEST_OPT?= -race -timeout $(GOTEST_TIMEOUT) -parallel 4 --tags=$(GO_BUILD_TAGS)
32-
GOTEST_INTEGRATION_OPT?= -race -timeout 360s -parallel 4
32+
GOTEST_INTEGRATION_OPT?= -race -timeout 360s -parallel 4 -skip Sudo
33+
GOTEST_INTEGRATION_OPT_SUDO= $(GOTEST_INTEGRATION_OPT) -exec sudo -run Sudo
3334
GOTEST_OPT_WITH_COVERAGE = $(GOTEST_OPT) -coverprofile=coverage.txt -covermode=atomic
3435
GOTEST_OPT_WITH_INTEGRATION=$(GOTEST_INTEGRATION_OPT) -tags=integration,$(GO_BUILD_TAGS)
36+
GOTEST_OPT_WITH_INTEGRATION_SUDO=$(GOTEST_INTEGRATION_OPT_SUDO) -tags=integration,$(GO_BUILD_TAGS)
3537
GOTEST_OPT_WITH_INTEGRATION_COVERAGE=$(GOTEST_OPT_WITH_INTEGRATION) -coverprofile=integration-coverage.txt -covermode=atomic
3638
GOCMD?= go
3739
GOOS=$(shell $(GOCMD) env GOOS)
@@ -152,12 +154,13 @@ endif
152154
runbuilttest: $(GOTESTSUM)
153155
ifneq (,$(wildcard ./builtunitetest.test))
154156
$(GOTESTSUM) --raw-command -- $(GOCMD) tool test2json -p "./..." -t ./builtunitetest.test -test.v -test.failfast -test.timeout $(GOTEST_TIMEOUT)
155-
endif
157+
endif
156158

157159
.PHONY: mod-integration-test
158160
mod-integration-test: $(GOTESTSUM)
159161
@echo "running $(GOCMD) integration test ./... in `pwd`"
160162
$(GOTESTSUM) $(GOTESTSUM_OPT) --packages="./..." -- $(GOTEST_OPT_WITH_INTEGRATION)
163+
$(GOTESTSUM) $(GOTESTSUM_OPT) --packages="./..." -- $(GOTEST_OPT_WITH_INTEGRATION_SUDO)
161164
@if [ -e integration-coverage.txt ]; then \
162165
$(GOCMD) tool cover -html=integration-coverage.txt -o integration-coverage.html; \
163166
fi

extension/cgroupruntimeextension/go.mod

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.22.0
44

55
require (
66
github.com/KimMachineGun/automemlimit v0.6.1
7+
github.com/containerd/cgroups/v3 v3.0.2
78
github.com/stretchr/testify v1.10.0
89
go.opentelemetry.io/collector/component v0.116.0
910
go.opentelemetry.io/collector/component/componenttest v0.116.0
@@ -13,29 +14,29 @@ require (
1314
go.uber.org/automaxprocs v1.6.0
1415
go.uber.org/goleak v1.3.0
1516
go.uber.org/zap v1.27.0
17+
golang.org/x/sys v0.27.0
1618
)
1719

1820
require (
1921
github.com/cilium/ebpf v0.9.1 // indirect
20-
github.com/containerd/cgroups/v3 v3.0.1 // indirect
21-
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
22+
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
2223
github.com/davecgh/go-spew v1.1.1 // indirect
23-
github.com/docker/go-units v0.4.0 // indirect
24+
github.com/docker/go-units v0.5.0 // indirect
2425
github.com/go-logr/logr v1.4.2 // indirect
2526
github.com/go-logr/stdr v1.2.2 // indirect
2627
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
27-
github.com/godbus/dbus/v5 v5.0.4 // indirect
28+
github.com/godbus/dbus/v5 v5.1.0 // indirect
2829
github.com/gogo/protobuf v1.3.2 // indirect
2930
github.com/google/uuid v1.6.0 // indirect
3031
github.com/knadh/koanf/maps v0.1.1 // indirect
3132
github.com/knadh/koanf/providers/confmap v0.1.0 // indirect
3233
github.com/knadh/koanf/v2 v2.1.2 // indirect
3334
github.com/mitchellh/copystructure v1.2.0 // indirect
3435
github.com/mitchellh/reflectwalk v1.0.2 // indirect
35-
github.com/opencontainers/runtime-spec v1.0.2 // indirect
36+
github.com/opencontainers/runtime-spec v1.1.0 // indirect
3637
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
3738
github.com/pmezard/go-difflib v1.0.0 // indirect
38-
github.com/sirupsen/logrus v1.8.1 // indirect
39+
github.com/sirupsen/logrus v1.9.0 // indirect
3940
go.opentelemetry.io/collector/config/configtelemetry v0.116.0 // indirect
4041
go.opentelemetry.io/collector/pdata v1.22.0 // indirect
4142
go.opentelemetry.io/otel v1.32.0 // indirect
@@ -45,7 +46,6 @@ require (
4546
go.opentelemetry.io/otel/trace v1.32.0 // indirect
4647
go.uber.org/multierr v1.11.0 // indirect
4748
golang.org/x/net v0.29.0 // indirect
48-
golang.org/x/sys v0.27.0 // indirect
4949
golang.org/x/text v0.18.0 // indirect
5050
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
5151
google.golang.org/grpc v1.68.1 // indirect

extension/cgroupruntimeextension/go.sum

Lines changed: 17 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
//go:build integration && linux
4+
// +build integration,linux
5+
6+
// Privileged access is required to set cgroup's memory and cpu max values
7+
8+
package cgroupruntimeextension // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/cgroupruntimeextension"
9+
10+
import (
11+
"context"
12+
"fmt"
13+
"math"
14+
"os"
15+
"path"
16+
"path/filepath"
17+
"runtime"
18+
"runtime/debug"
19+
"strconv"
20+
"strings"
21+
"testing"
22+
23+
"github.com/containerd/cgroups/v3/cgroup2"
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
26+
"go.opentelemetry.io/collector/component/componenttest"
27+
"go.opentelemetry.io/collector/extension/extensiontest"
28+
"golang.org/x/sys/unix"
29+
)
30+
31+
const (
32+
defaultCgroup2Path = "/sys/fs/cgroup"
33+
)
34+
35+
// checkCgroupSystem skips the test if is not run in a cgroupv2 system
36+
func checkCgroupSystem(tb testing.TB) {
37+
var st unix.Statfs_t
38+
err := unix.Statfs(defaultCgroup2Path, &st)
39+
if err != nil {
40+
tb.Skip("cannot statfs cgroup root")
41+
}
42+
43+
isUnified := st.Type == unix.CGROUP2_SUPER_MAGIC
44+
if !isUnified {
45+
tb.Skip("System running in hybrid or cgroupv1 mode")
46+
}
47+
}
48+
49+
// cgroupMaxCpu returns the CPU max definition for a given cgroup slice path
50+
// File format: cpu_quote cpu_period
51+
func cgroupMaxCpu(filename string) (quota int64, period uint64, err error) {
52+
out, err := os.ReadFile(filepath.Join(defaultCgroup2Path, filename, "cpu.max"))
53+
if err != nil {
54+
return 0, 0, err
55+
}
56+
values := strings.Split(strings.TrimSpace(string(out)), " ")
57+
if values[0] == "max" {
58+
quota = math.MaxInt64
59+
} else {
60+
quota, _ = strconv.ParseInt(values[0], 10, 64)
61+
}
62+
period, _ = strconv.ParseUint(values[1], 10, 64)
63+
return quota, period, err
64+
}
65+
66+
func TestCgroupV2SudoIntegration(t *testing.T) {
67+
checkCgroupSystem(t)
68+
pointerInt64 := func(val int64) *int64 {
69+
return &val
70+
}
71+
pointerUint64 := func(uval uint64) *uint64 {
72+
return &uval
73+
}
74+
75+
tests := []struct {
76+
name string
77+
// nil CPU quota == "max" cgroup string value
78+
cgroupCpuQuota *int64
79+
cgroupCpuPeriod uint64
80+
cgroupMaxMemory int64
81+
config *Config
82+
expectedGoMaxProcs int
83+
expectedGoMemLimit int64
84+
}{
85+
{
86+
name: "90% the max cgroup memory and 12 GOMAXPROCS",
87+
cgroupCpuQuota: pointerInt64(100000),
88+
cgroupCpuPeriod: 8000,
89+
// 128 Mb
90+
cgroupMaxMemory: 134217728,
91+
config: &Config{
92+
GoMaxProcs: GoMaxProcsConfig{
93+
Enabled: true,
94+
},
95+
GoMemLimit: GoMemLimitConfig{
96+
Enabled: true,
97+
Ratio: 0.9,
98+
},
99+
},
100+
// 100000 / 8000
101+
expectedGoMaxProcs: 12,
102+
// 134217728 * 0.9
103+
expectedGoMemLimit: 120795955,
104+
},
105+
{
106+
name: "50% of the max cgroup memory and 1 GOMAXPROCS",
107+
cgroupCpuQuota: pointerInt64(100000),
108+
cgroupCpuPeriod: 100000,
109+
// 128 Mb
110+
cgroupMaxMemory: 134217728,
111+
config: &Config{
112+
GoMaxProcs: GoMaxProcsConfig{
113+
Enabled: true,
114+
},
115+
GoMemLimit: GoMemLimitConfig{
116+
Enabled: true,
117+
Ratio: 0.5,
118+
},
119+
},
120+
// 100000 / 100000
121+
expectedGoMaxProcs: 1,
122+
// 134217728 * 0.5
123+
expectedGoMemLimit: 67108864,
124+
},
125+
{
126+
name: "10% of the max cgroup memory, max cpu, default GOMAXPROCS",
127+
cgroupCpuQuota: nil,
128+
cgroupCpuPeriod: 100000,
129+
// 128 Mb
130+
cgroupMaxMemory: 134217728,
131+
config: &Config{
132+
GoMaxProcs: GoMaxProcsConfig{
133+
Enabled: true,
134+
},
135+
GoMemLimit: GoMemLimitConfig{
136+
Enabled: true,
137+
Ratio: 0.1,
138+
},
139+
},
140+
// GOMAXPROCS is set to the value of `cpu.max / cpu.period`
141+
// If cpu.max is set to max, GOMAXPROCS should not be
142+
// modified
143+
expectedGoMaxProcs: runtime.GOMAXPROCS(-1),
144+
// 134217728 * 0.1
145+
expectedGoMemLimit: 13421772,
146+
},
147+
}
148+
149+
cgroupPath, err := cgroup2.PidGroupPath(os.Getpid())
150+
assert.NoError(t, err)
151+
manager, err := cgroup2.Load(cgroupPath)
152+
assert.NoError(t, err)
153+
154+
stats, err := manager.Stat()
155+
require.NoError(t, err)
156+
157+
// Startup resource values
158+
initialMaxMemory := stats.GetMemory().GetUsageLimit()
159+
memoryCgroupCleanUp := func() {
160+
err = manager.Update(&cgroup2.Resources{
161+
Memory: &cgroup2.Memory{
162+
Max: pointerInt64(int64(initialMaxMemory)),
163+
},
164+
})
165+
assert.NoError(t, err)
166+
}
167+
168+
if initialMaxMemory == math.MaxUint64 {
169+
// fallback solution to set cgroup's max memory to "max"
170+
memoryCgroupCleanUp = func() {
171+
err = os.WriteFile(path.Join(defaultCgroup2Path, cgroupPath, "memory.max"), []byte("max"), 0o600)
172+
assert.NoError(t, err)
173+
}
174+
}
175+
176+
initialCpuQuota, initialCpuPeriod, err := cgroupMaxCpu(cgroupPath)
177+
require.NoError(t, err)
178+
cpuCgroupCleanUp := func() {
179+
fmt.Println(initialCpuQuota)
180+
err = manager.Update(&cgroup2.Resources{
181+
CPU: &cgroup2.CPU{
182+
Max: cgroup2.NewCPUMax(pointerInt64(initialCpuQuota), pointerUint64(initialCpuPeriod)),
183+
},
184+
})
185+
assert.NoError(t, err)
186+
}
187+
188+
if initialCpuQuota == math.MaxInt64 {
189+
// fallback solution to set cgroup's max cpu to "max"
190+
cpuCgroupCleanUp = func() {
191+
err = os.WriteFile(path.Join(defaultCgroup2Path, cgroupPath, "cpu.max"), []byte("max"), 0o600)
192+
assert.NoError(t, err)
193+
}
194+
}
195+
196+
initialGoMem := debug.SetMemoryLimit(-1)
197+
initialGoProcs := runtime.GOMAXPROCS(-1)
198+
199+
for _, test := range tests {
200+
t.Run(test.name, func(t *testing.T) {
201+
// restore startup cgroup initial resource values
202+
t.Cleanup(func() {
203+
debug.SetMemoryLimit(initialGoMem)
204+
runtime.GOMAXPROCS(initialGoProcs)
205+
memoryCgroupCleanUp()
206+
cpuCgroupCleanUp()
207+
})
208+
209+
err = manager.Update(&cgroup2.Resources{
210+
Memory: &cgroup2.Memory{
211+
// Default max memory must be
212+
// overwritten
213+
// to automemlimit change the GOMEMLIMIT
214+
// value
215+
Max: pointerInt64(test.cgroupMaxMemory),
216+
},
217+
CPU: &cgroup2.CPU{
218+
Max: cgroup2.NewCPUMax(test.cgroupCpuQuota, pointerUint64(test.cgroupCpuPeriod)),
219+
},
220+
})
221+
require.NoError(t, err)
222+
223+
factory := NewFactory()
224+
ctx := context.Background()
225+
extension, err := factory.Create(ctx, extensiontest.NewNopSettings(), test.config)
226+
require.NoError(t, err)
227+
228+
err = extension.Start(ctx, componenttest.NewNopHost())
229+
require.NoError(t, err)
230+
231+
assert.Equal(t, test.expectedGoMaxProcs, runtime.GOMAXPROCS(-1))
232+
assert.Equal(t, test.expectedGoMemLimit, debug.SetMemoryLimit(-1))
233+
})
234+
}
235+
}

0 commit comments

Comments
 (0)