Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 765e705

Browse files
chore: use container memory if containerised for oom notifications (coder#17062)
Currently we query only the underlying host's memory usage for our memory resource monitor. This PR changes that to check if the workspace is in a container, and if so it queries the container's memory usage, falling back to the host's memory usage if not.
1 parent 674f60f commit 765e705

File tree

4 files changed

+156
-8
lines changed

4 files changed

+156
-8
lines changed

agent/agent.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,10 @@ func (a *agent) run() (retErr error) {
965965
if err != nil {
966966
return xerrors.Errorf("failed to create resources fetcher: %w", err)
967967
}
968-
resourcesFetcher := resourcesmonitor.NewFetcher(statfetcher)
968+
resourcesFetcher, err := resourcesmonitor.NewFetcher(statfetcher)
969+
if err != nil {
970+
return xerrors.Errorf("new resource fetcher: %w", err)
971+
}
969972

970973
resourcesmonitor := resourcesmonitor.NewResourcesMonitor(logger, clk, config, resourcesFetcher, aAPI)
971974
return resourcesmonitor.Start(ctx)

agent/proto/resourcesmonitor/fetcher.go

+39-7
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,58 @@ import (
66
"github.com/coder/coder/v2/cli/clistat"
77
)
88

9+
type Statter interface {
10+
IsContainerized() (bool, error)
11+
ContainerMemory(p clistat.Prefix) (*clistat.Result, error)
12+
HostMemory(p clistat.Prefix) (*clistat.Result, error)
13+
Disk(p clistat.Prefix, path string) (*clistat.Result, error)
14+
}
15+
916
type Fetcher interface {
1017
FetchMemory() (total int64, used int64, err error)
1118
FetchVolume(volume string) (total int64, used int64, err error)
1219
}
1320

1421
type fetcher struct {
15-
*clistat.Statter
22+
Statter
23+
isContainerized bool
1624
}
1725

1826
//nolint:revive
19-
func NewFetcher(f *clistat.Statter) *fetcher {
20-
return &fetcher{
21-
f,
27+
func NewFetcher(f Statter) (*fetcher, error) {
28+
isContainerized, err := f.IsContainerized()
29+
if err != nil {
30+
return nil, xerrors.Errorf("check is containerized: %w", err)
2231
}
32+
33+
return &fetcher{f, isContainerized}, nil
2334
}
2435

2536
func (f *fetcher) FetchMemory() (total int64, used int64, err error) {
26-
mem, err := f.HostMemory(clistat.PrefixDefault)
27-
if err != nil {
28-
return 0, 0, xerrors.Errorf("failed to fetch memory: %w", err)
37+
var mem *clistat.Result
38+
39+
if f.isContainerized {
40+
mem, err = f.ContainerMemory(clistat.PrefixDefault)
41+
if err != nil {
42+
return 0, 0, xerrors.Errorf("fetch container memory: %w", err)
43+
}
44+
45+
// A container might not have a memory limit set. If this
46+
// happens we want to fallback to querying the host's memory
47+
// to know what the total memory is on the host.
48+
if mem.Total == nil {
49+
hostMem, err := f.HostMemory(clistat.PrefixDefault)
50+
if err != nil {
51+
return 0, 0, xerrors.Errorf("fetch host memory: %w", err)
52+
}
53+
54+
mem.Total = hostMem.Total
55+
}
56+
} else {
57+
mem, err = f.HostMemory(clistat.PrefixDefault)
58+
if err != nil {
59+
return 0, 0, xerrors.Errorf("fetch host memory: %w", err)
60+
}
2961
}
3062

3163
if mem.Total == nil {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package resourcesmonitor_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/v2/agent/proto/resourcesmonitor"
10+
"github.com/coder/coder/v2/cli/clistat"
11+
"github.com/coder/coder/v2/coderd/util/ptr"
12+
)
13+
14+
type mockStatter struct {
15+
isContainerized bool
16+
containerMemory clistat.Result
17+
hostMemory clistat.Result
18+
disk map[string]clistat.Result
19+
}
20+
21+
func (s *mockStatter) IsContainerized() (bool, error) {
22+
return s.isContainerized, nil
23+
}
24+
25+
func (s *mockStatter) ContainerMemory(_ clistat.Prefix) (*clistat.Result, error) {
26+
return &s.containerMemory, nil
27+
}
28+
29+
func (s *mockStatter) HostMemory(_ clistat.Prefix) (*clistat.Result, error) {
30+
return &s.hostMemory, nil
31+
}
32+
33+
func (s *mockStatter) Disk(_ clistat.Prefix, path string) (*clistat.Result, error) {
34+
disk, ok := s.disk[path]
35+
if !ok {
36+
return nil, xerrors.New("path not found")
37+
}
38+
return &disk, nil
39+
}
40+
41+
func TestFetchMemory(t *testing.T) {
42+
t.Parallel()
43+
44+
t.Run("IsContainerized", func(t *testing.T) {
45+
t.Parallel()
46+
47+
t.Run("WithMemoryLimit", func(t *testing.T) {
48+
t.Parallel()
49+
50+
fetcher, err := resourcesmonitor.NewFetcher(&mockStatter{
51+
isContainerized: true,
52+
containerMemory: clistat.Result{
53+
Used: 10.0,
54+
Total: ptr.Ref(20.0),
55+
},
56+
hostMemory: clistat.Result{
57+
Used: 20.0,
58+
Total: ptr.Ref(30.0),
59+
},
60+
})
61+
require.NoError(t, err)
62+
63+
total, used, err := fetcher.FetchMemory()
64+
require.NoError(t, err)
65+
require.Equal(t, int64(10), used)
66+
require.Equal(t, int64(20), total)
67+
})
68+
69+
t.Run("WithoutMemoryLimit", func(t *testing.T) {
70+
t.Parallel()
71+
72+
fetcher, err := resourcesmonitor.NewFetcher(&mockStatter{
73+
isContainerized: true,
74+
containerMemory: clistat.Result{
75+
Used: 10.0,
76+
Total: nil,
77+
},
78+
hostMemory: clistat.Result{
79+
Used: 20.0,
80+
Total: ptr.Ref(30.0),
81+
},
82+
})
83+
require.NoError(t, err)
84+
85+
total, used, err := fetcher.FetchMemory()
86+
require.NoError(t, err)
87+
require.Equal(t, int64(10), used)
88+
require.Equal(t, int64(30), total)
89+
})
90+
})
91+
92+
t.Run("IsHost", func(t *testing.T) {
93+
t.Parallel()
94+
95+
fetcher, err := resourcesmonitor.NewFetcher(&mockStatter{
96+
isContainerized: false,
97+
hostMemory: clistat.Result{
98+
Used: 20.0,
99+
Total: ptr.Ref(30.0),
100+
},
101+
})
102+
require.NoError(t, err)
103+
104+
total, used, err := fetcher.FetchMemory()
105+
require.NoError(t, err)
106+
require.Equal(t, int64(20), used)
107+
require.Equal(t, int64(30), total)
108+
})
109+
}

cli/clistat/container.go

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ const (
1616
kubernetesDefaultServiceAccountToken = "/var/run/secrets/kubernetes.io/serviceaccount/token" //nolint:gosec
1717
)
1818

19+
func (s *Statter) IsContainerized() (ok bool, err error) {
20+
return IsContainerized(s.fs)
21+
}
22+
1923
// IsContainerized returns whether the host is containerized.
2024
// This is adapted from https://github.com/elastic/go-sysinfo/tree/main/providers/linux/container.go#L31
2125
// with modifications to support Sysbox containers.

0 commit comments

Comments
 (0)