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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 39 additions & 7 deletions
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 {
Lines changed: 109 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy