Skip to content

Commit

Permalink
fix(cgroupv2): handle undefined CPU quota (#9)
Browse files Browse the repository at this point in the history
* fix(cgroupv2): handle undefined CPU quota

* refactor: use os.IsNotExist instead of errors.Is()

* test: add loadCPUQuota tests

* docs: update README.md
  • Loading branch information
mingrammer authored Nov 3, 2022
1 parent 465d059 commit 7471a05
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 5 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

![Run tests](https://github.com/daangn/autopprof/workflows/Run%20tests/badge.svg) [![Release](https://img.shields.io/github/v/tag/daangn/autopprof?label=Release)](https://github.com/daangn/autopprof/releases)

Automatically profile the Go applications when CPU or memory utilization crosses
threshold levels.
Automatically profile the Go applications when CPU or memory utilization crosses specific
threshold levels against the Linux container CPU quota and memory limit.

Once you start the autopprof, the autopprof process will periodically check the CPU and
memory utilization of the Go applications. If the resource utilization crosses the
Expand Down Expand Up @@ -61,7 +61,7 @@ func main() {
}
```

> You can create the custom reporter by implementing the `report.Reporter` interface.
> You can create a custom reporter by implementing the `report.Reporter` interface.
## Benchmark

Expand Down
22 changes: 21 additions & 1 deletion autopprof.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func Start(opt Option) error {
ap.memThreshold = opt.MemThreshold
}
if !ap.disableCPUProf {
if err := ap.queryer.setCPUQuota(); err != nil {
if err := ap.loadCPUQuota(); err != nil {
return err
}
}
Expand All @@ -106,6 +106,26 @@ func Stop() {
}
}

func (ap *autoPprof) loadCPUQuota() error {
err := ap.queryer.setCPUQuota()
if err == nil {
return nil
}

// If memory profiling is disabled and CPU quota isn't set,
// returns an error immediately.
if ap.disableMemProf {
return err
}
// If memory profiling is enabled, just logs the error and
// disables the cpu profiling.
log.Println(
"autopprof: disable the cpu profiling due to the CPU quota isn't set",
)
ap.disableCPUProf = true
return nil
}

func (ap *autoPprof) watch() {
go ap.watchCPUUsage()
go ap.watchMemUsage()
Expand Down
79 changes: 79 additions & 0 deletions autopprof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,85 @@ func TestStop(t *testing.T) {
}
}

func TestAutoPprof_loadCPUQuota(t *testing.T) {
testCases := []struct {
name string
newAp func() *autoPprof
wantDisableCPUProfFlag bool
wantErr error
}{
{
name: "cpu quota is set",
newAp: func() *autoPprof {
ctrl := gomock.NewController(t)

mockQueryer := NewMockqueryer(ctrl)
mockQueryer.EXPECT().
setCPUQuota().
Return(nil) // Means that the quota is set correctly.

return &autoPprof{
queryer: mockQueryer,
disableCPUProf: false,
disableMemProf: false,
}
},
wantDisableCPUProfFlag: false,
wantErr: nil,
},
{
name: "cpu quota isn't set and memory profiling is enabled",
newAp: func() *autoPprof {
ctrl := gomock.NewController(t)

mockQueryer := NewMockqueryer(ctrl)
mockQueryer.EXPECT().
setCPUQuota().
Return(ErrV2CPUQuotaUndefined)

return &autoPprof{
queryer: mockQueryer,
disableCPUProf: false,
disableMemProf: false,
}
},
wantDisableCPUProfFlag: true,
wantErr: nil,
},
{
name: "cpu quota isn't set and memory profiling is disabled",
newAp: func() *autoPprof {
ctrl := gomock.NewController(t)

mockQueryer := NewMockqueryer(ctrl)
mockQueryer.EXPECT().
setCPUQuota().
Return(ErrV2CPUQuotaUndefined)

return &autoPprof{
queryer: mockQueryer,
disableCPUProf: false,
disableMemProf: true,
}
},
wantDisableCPUProfFlag: false,
wantErr: ErrV2CPUQuotaUndefined,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ap := tc.newAp()
err := ap.loadCPUQuota()
if !errors.Is(err, tc.wantErr) {
t.Errorf("loadCPUQuota() = %v, want %v", err, tc.wantErr)
}
if ap.disableCPUProf != tc.wantDisableCPUProfFlag {
t.Errorf("disableCPUProf = %v, want %v", ap.disableCPUProf, tc.wantDisableCPUProfFlag)
}
})
}
}

func TestAutoPprof_watchCPUUsage(t *testing.T) {
ctrl := gomock.NewController(t)

Expand Down
2 changes: 2 additions & 0 deletions cgroups.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"github.com/containerd/cgroups"
)

//go:generate mockgen -source=cgroups.go -destination=cgroups_mock.go -package=autopprof

type queryer interface {
cpuUsage() (float64, error)
memUsage() (float64, error)
Expand Down
78 changes: 78 additions & 0 deletions cgroups_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion cgroupv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import (

const (
cgroupV2MountPoint = "/sys/fs/cgroup"
cgroupV2CPUMaxFile = "cpu.max"

cgroupV2CPUMaxFile = "cpu.max"
cgroupV2CPUMaxQuotaMax = "max"

cgroupV2CPUMaxDefaultPeriod = 100000
)
Expand Down Expand Up @@ -46,6 +48,9 @@ func (c *cgroupV2) setCPUQuota() error {
f, err := os.Open(
path.Join(c.mountPoint, c.cpuMaxFile),
)
if os.IsNotExist(err) {
return ErrV2CPUQuotaUndefined
}
if err != nil {
return err
}
Expand All @@ -58,6 +63,9 @@ func (c *cgroupV2) setCPUQuota() error {
"autopprof: invalid cpu.max format",
)
}
if fields[0] == cgroupV2CPUMaxQuotaMax {
return ErrV2CPUQuotaUndefined
}

max, err := strconv.Atoi(fields[0])
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
)
ErrNilReporter = fmt.Errorf("autopprof: Reporter can't be nil")
ErrDisableAllProfiling = fmt.Errorf("autopprof: all profiling is disabled")
ErrV2CPUQuotaUndefined = fmt.Errorf("autopprof: v2 cpu quota is undefined")
ErrV2CPUMaxEmpty = fmt.Errorf("autopprof: v2 cpu.max is empty")
ErrV1CPUSubsystemEmpty = fmt.Errorf("autopprof: v1 cpu subsystem is empty")
)

0 comments on commit 7471a05

Please sign in to comment.