pomerium/pkg/envoy/resource_monitor_test.go
Joe Kralicky 927f24e1ff
Envoy resource monitoring & overload manager configuration (#5106)
* Initial envoy cgroup resource monitor implementation

* Add cgroupv1 support; add metrics instrumentation

* Slight refactor for more efficient memory limit detection

Instead of reading memory.max/limit_in_bytes on every tick, we
read it once, then again only when it is modified.

To support this change, logic for computing the saturation was moved out
of the cgroup driver and into the resource monitor, and the driver
interface now has separate methods for reading memory usage and limit.

* Code cleanup/lint fixes

* Add platform build tags

* Add unit tests

* Fix lint issues

* Add runtime flag to allow disabling resource monitor

* Clamp saturation values to the range [0.0, 1.0]

* Switch to x/sys/unix; handle inotify IN_IGNORED events
2024-05-28 16:57:09 -04:00

886 lines
23 KiB
Go

//go:build linux
package envoy
import (
"context"
"fmt"
"io/fs"
"maps"
"os"
"path"
"path/filepath"
"testing"
"testing/fstest"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/config/envoyconfig"
"github.com/pomerium/pomerium/config/envoyconfig/filemgr"
"github.com/pomerium/pomerium/internal/testutil"
)
var (
file = func(data string, mode fs.FileMode) *fstest.MapFile {
// ensure the data always ends with a \n
if data != "" && data[len(data)-1] != '\n' {
data += "\n"
}
return &fstest.MapFile{Data: []byte(data), Mode: mode}
}
v2Fs = fstest.MapFS{
"sys/fs/cgroup/test/cgroup.type": file("domain", 0o644),
"sys/fs/cgroup/test/cgroup.controllers": file("memory", 0o444),
"sys/fs/cgroup/test/cgroup.subtree_control": file("", 0o644),
"sys/fs/cgroup/test/memory.current": file("100", 0o644),
"sys/fs/cgroup/test/memory.max": file("200", 0o644),
"proc/1/cgroup": file("0::/test\n", 0o444),
"proc/2/cgroup": file("0::/test2 (deleted)\n", 0o444),
"proc/1/mountinfo": file(`
24 30 0:22 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw
25 30 0:23 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sys rw
33 25 0:28 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:9 - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot
`[1:], 0o444),
}
v1Fs = fstest.MapFS{
"sys/fs/cgroup/memory/test/memory.usage_in_bytes": file("100", 0o644),
"sys/fs/cgroup/memory/test/memory.limit_in_bytes": file("200", 0o644),
"proc/1/cgroup": file(`
1:memory:/test
0::/test
`[1:], 0o444),
"proc/1/mountinfo": file(`
26 31 0:24 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw
27 31 0:5 / /proc rw,nosuid,nodev,noexec,relatime shared:14 - proc proc rw
31 1 252:1 / / rw,relatime shared:1 - ext4 /dev/vda1 rw,errors=remount-ro
35 26 0:29 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755
40 35 0:34 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,memory
`[1:], 0o444),
}
v1ContainerFs = fstest.MapFS{
"sys/fs/cgroup/memory/test/memory.usage_in_bytes": file("100", 0o644),
"sys/fs/cgroup/memory/test/memory.limit_in_bytes": file("200", 0o644),
"proc/1/cgroup": file(`
1:memory:/test
0::/test
`[1:], 0o444),
"proc/1/mountinfo": file(`
1574 1573 0:138 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
1578 1573 0:133 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro
1579 1578 0:141 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
1586 1579 0:39 /test /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,memory
1311 1574 0:138 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw
`[1:], 0o444),
}
hybridFs = fstest.MapFS{
"sys/fs/cgroup/memory/test/memory.usage_in_bytes": file("100", 0o644),
"sys/fs/cgroup/memory/test/memory.limit_in_bytes": file("200", 0o644),
"sys/fs/cgroup/unified/test/cgroup.type": file("domain", 0o644),
"sys/fs/cgroup/unified/test/cgroup.controllers": file("memory", 0o444),
"sys/fs/cgroup/unified/test/cgroup.subtree_control": file("", 0o644),
"sys/fs/cgroup/unified/test/memory.current": file("100", 0o644),
"sys/fs/cgroup/unified/test/memory.max": file("200", 0o644),
"proc/1/cgroup": file(`
1:memory:/test
0::/test
`[1:], 0o444),
"proc/1/mountinfo": file(`
26 31 0:24 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw
27 31 0:5 / /proc rw,nosuid,nodev,noexec,relatime shared:14 - proc proc rw
35 26 0:29 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755
36 35 0:30 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime shared:10 - cgroup2 cgroup2 rw,nsdelegate
46 35 0:40 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,memory
`[1:], 0o444),
}
with = func(dest, src fstest.MapFS) fstest.MapFS {
dest = maps.Clone(dest)
for k, v := range src {
dest[k] = v
}
return dest
}
without = func(fs fstest.MapFS, keys ...string) fstest.MapFS {
fs = maps.Clone(fs)
for _, k := range keys {
delete(fs, k)
}
return fs
}
)
func TestCgroupV2Driver(t *testing.T) {
d := cgroupV2Driver{
root: "sys/fs/cgroup",
fs: v2Fs,
}
t.Run("Path", func(t *testing.T) {
assert.Equal(t, "sys/fs/cgroup", d.Path("test", RootPath))
assert.Equal(t, "sys/fs/cgroup/test/memory.current", d.Path("test", MemoryUsagePath))
assert.Equal(t, "sys/fs/cgroup/test/memory.max", d.Path("test", MemoryLimitPath))
assert.Equal(t, "", d.Path("test", CgroupFilePath(0xF00)))
})
t.Run("CgroupForPid", func(t *testing.T) {
cgroup, err := d.CgroupForPid(1)
assert.NoError(t, err)
assert.Equal(t, "/test", cgroup)
cgroup, err = d.CgroupForPid(2)
assert.NoError(t, err)
assert.Equal(t, "/test2", cgroup)
_, err = d.CgroupForPid(12345)
assert.Error(t, err)
})
t.Run("MemoryUsage", func(t *testing.T) {
cases := []struct {
fs fstest.MapFS
err string
usage uint64
}{
0: {
fs: v2Fs,
usage: 100,
},
1: {
fs: with(v2Fs, fstest.MapFS{
"sys/fs/cgroup/test/memory.current": file("invalid", 0o644),
}),
err: "strconv.ParseUint: parsing \"invalid\": invalid syntax",
},
2: {
fs: without(v2Fs, "sys/fs/cgroup/test/memory.current"),
err: "open sys/fs/cgroup/test/memory.current: file does not exist",
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
driver := cgroupV2Driver{
root: "sys/fs/cgroup",
fs: c.fs,
}
usage, err := driver.MemoryUsage("test")
if c.err == "" {
assert.NoError(t, err)
assert.Equal(t, c.usage, usage)
} else {
assert.EqualError(t, err, c.err)
}
})
}
})
t.Run("MemoryLimit", func(t *testing.T) {
cases := []struct {
fs fstest.MapFS
err string
limit uint64
}{
0: {
fs: v2Fs,
limit: 200,
},
1: {
fs: with(v2Fs, fstest.MapFS{
"sys/fs/cgroup/test/memory.max": file("max", 0o644),
}),
limit: 0,
},
2: {
fs: without(v2Fs, "sys/fs/cgroup/test/memory.max"),
err: "open sys/fs/cgroup/test/memory.max: file does not exist",
},
3: {
fs: with(v2Fs, fstest.MapFS{
"sys/fs/cgroup/test/memory.max": file("invalid", 0o644),
}),
err: "strconv.ParseUint: parsing \"invalid\": invalid syntax",
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
driver := cgroupV2Driver{
root: "sys/fs/cgroup",
fs: c.fs,
}
limit, err := driver.MemoryLimit("test")
if c.err == "" {
assert.NoError(t, err)
assert.Equal(t, c.limit, limit)
} else {
assert.EqualError(t, err, c.err)
}
})
}
})
t.Run("Validate", func(t *testing.T) {
cases := []struct {
fs fstest.MapFS
root string // optional
err string
}{
0: {fs: v2Fs},
1: {fs: hybridFs, root: "sys/fs/cgroup/unified"},
2: {
fs: with(v2Fs, fstest.MapFS{
"sys/fs/cgroup/test/cgroup.type": file("threaded", 0o644),
}),
err: "not a domain cgroup",
},
3: {
fs: with(v2Fs, fstest.MapFS{
"sys/fs/cgroup/test/cgroup.subtree_control": file("cpu", 0o644),
}),
err: "not a leaf cgroup",
},
4: {
fs: with(v2Fs, fstest.MapFS{
"sys/fs/cgroup/test/cgroup.controllers": file("cpu io", 0o444),
}),
err: "memory controller not enabled",
},
5: {
fs: without(v2Fs, "sys/fs/cgroup/test/cgroup.controllers"),
err: "open sys/fs/cgroup/test/cgroup.controllers: file does not exist",
},
6: {
fs: without(v2Fs, "sys/fs/cgroup/test/cgroup.type"),
err: "open sys/fs/cgroup/test/cgroup.type: file does not exist",
},
7: {
fs: without(v2Fs, "sys/fs/cgroup/test/cgroup.subtree_control"),
err: "open sys/fs/cgroup/test/cgroup.subtree_control: file does not exist",
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
driver := cgroupV2Driver{
root: "sys/fs/cgroup",
fs: c.fs,
}
if c.root != "" {
driver.root = c.root
}
err := driver.Validate("test")
if c.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, c.err)
}
})
}
})
}
func TestCgroupV1Driver(t *testing.T) {
d := cgroupV1Driver{
root: "sys/fs/cgroup",
fs: v1Fs,
}
t.Run("Path", func(t *testing.T) {
assert.Equal(t, "sys/fs/cgroup", d.Path("test", RootPath))
assert.Equal(t, "sys/fs/cgroup/memory/test/memory.usage_in_bytes", d.Path("test", MemoryUsagePath))
assert.Equal(t, "sys/fs/cgroup/memory/test/memory.limit_in_bytes", d.Path("test", MemoryLimitPath))
assert.Equal(t, "", d.Path("test", CgroupFilePath(0xF00)))
})
t.Run("CgroupForPid", func(t *testing.T) {
cgroup, err := d.CgroupForPid(1)
assert.NoError(t, err)
assert.Equal(t, "/test", cgroup)
_, err = d.CgroupForPid(12345)
assert.Error(t, err)
})
t.Run("MemoryUsage", func(t *testing.T) {
cases := []struct {
fs fstest.MapFS
err string
usage uint64
}{
0: {
fs: v1Fs,
usage: 100,
},
1: {
fs: with(v1Fs, fstest.MapFS{
"sys/fs/cgroup/memory/test/memory.usage_in_bytes": file("invalid", 0o644),
}),
err: "strconv.ParseUint: parsing \"invalid\": invalid syntax",
},
2: {
fs: without(v1Fs, "sys/fs/cgroup/memory/test/memory.usage_in_bytes"),
err: "open sys/fs/cgroup/memory/test/memory.usage_in_bytes: file does not exist",
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
driver := cgroupV1Driver{
root: "sys/fs/cgroup",
fs: c.fs,
}
usage, err := driver.MemoryUsage("test")
if c.err == "" {
assert.NoError(t, err)
assert.Equal(t, c.usage, usage)
} else {
assert.EqualError(t, err, c.err)
}
})
}
})
t.Run("MemoryLimit", func(t *testing.T) {
cases := []struct {
fs fstest.MapFS
err string
limit uint64
}{
0: {
fs: v1Fs,
limit: 200,
},
1: {
fs: with(v1Fs, fstest.MapFS{
"sys/fs/cgroup/memory/test/memory.limit_in_bytes": file("max", 0o644),
}),
limit: 0,
},
2: {
fs: with(v1Fs, fstest.MapFS{
"sys/fs/cgroup/memory/test/memory.limit_in_bytes": file("invalid", 0o644),
}),
err: "strconv.ParseUint: parsing \"invalid\": invalid syntax",
},
3: {
fs: without(v1Fs, "sys/fs/cgroup/memory/test/memory.limit_in_bytes"),
err: "open sys/fs/cgroup/memory/test/memory.limit_in_bytes: file does not exist",
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
driver := cgroupV1Driver{
root: "sys/fs/cgroup",
fs: c.fs,
}
limit, err := driver.MemoryLimit("test")
if c.err == "" {
assert.NoError(t, err)
assert.Equal(t, c.limit, limit)
} else {
assert.EqualError(t, err, c.err)
}
})
}
})
t.Run("Validate", func(t *testing.T) {
cases := []struct {
fs fstest.MapFS
err string
}{
0: {fs: v1Fs},
1: {fs: v1ContainerFs},
2: {fs: hybridFs},
3: {
fs: without(v1Fs,
"sys/fs/cgroup/memory/test/memory.usage_in_bytes",
"sys/fs/cgroup/memory/test/memory.limit_in_bytes",
),
err: "memory controller not enabled",
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
driver := cgroupV1Driver{
root: "sys/fs/cgroup",
fs: c.fs,
}
err := driver.Validate("test")
if c.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, c.err)
}
})
}
})
t.Run("Container FS", func(t *testing.T) {
driver := cgroupV1Driver{
root: "sys/fs/cgroup",
fs: v1ContainerFs,
}
cgroup, err := driver.CgroupForPid(1)
assert.NoError(t, err)
assert.Equal(t, "/", cgroup)
})
t.Run("Hybrid FS", func(t *testing.T) {
driver := cgroupV1Driver{
root: "sys/fs/cgroup",
fs: hybridFs,
}
cgroup, err := driver.CgroupForPid(1)
assert.NoError(t, err)
assert.Equal(t, "/test", cgroup)
driver2 := cgroupV2Driver{
root: "sys/fs/cgroup/unified",
fs: hybridFs,
}
cgroup, err = driver2.CgroupForPid(1)
assert.NoError(t, err)
assert.Equal(t, "/test", cgroup)
})
}
func TestFindMountpoint(t *testing.T) {
withActualPid := func(fs fstest.MapFS) fstest.MapFS {
fs = maps.Clone(fs)
fs[fmt.Sprintf("proc/%d/cgroup", os.Getpid())] = fs["proc/1/cgroup"]
fs[fmt.Sprintf("proc/%d/mountinfo", os.Getpid())] = fs["proc/1/mountinfo"]
return fs
}
cases := []struct {
fsys fs.FS
mountpoint string
isV2 bool
err string
}{
0: {
fsys: withActualPid(v2Fs),
mountpoint: "sys/fs/cgroup",
isV2: true,
},
1: {
fsys: withActualPid(v1Fs),
mountpoint: "sys/fs/cgroup",
isV2: false,
},
2: {
fsys: withActualPid(hybridFs),
mountpoint: "sys/fs/cgroup/unified",
isV2: true,
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
mountpoint, isV2, err := findMountpoint(c.fsys)
if c.err == "" {
assert.NoError(t, err)
assert.Equal(t, c.mountpoint, mountpoint)
assert.Equal(t, c.isV2, isV2)
} else {
assert.EqualError(t, err, c.err)
}
})
}
}
type hybridTestFS struct {
base fstest.MapFS
tempDir string
}
var _ fs.FS = (*hybridTestFS)(nil)
func (fs *hybridTestFS) Open(name string) (fs.File, error) {
switch base := path.Base(name); base {
case "memory.current", "memory.max":
return os.Open(filepath.Join(fs.tempDir, ".fs", base))
}
return fs.base.Open(name)
}
func (fs *hybridTestFS) ReadFile(name string) ([]byte, error) {
switch base := path.Base(name); base {
case "memory.current", "memory.max":
return os.ReadFile(filepath.Join(fs.tempDir, ".fs", base))
}
return fs.base.ReadFile(name)
}
func (fs *hybridTestFS) Stat(name string) (fs.FileInfo, error) {
switch base := path.Base(name); base {
case "memory.current", "memory.max":
return os.Stat(filepath.Join(fs.tempDir, ".fs", base))
}
return fs.base.Stat(name)
}
type pathOverrideDriver struct {
CgroupDriver
overrides map[CgroupFilePath]string
}
var _ CgroupDriver = (*pathOverrideDriver)(nil)
func (d *pathOverrideDriver) Path(name string, path CgroupFilePath) string {
if override, ok := d.overrides[path]; ok {
return override
}
return d.CgroupDriver.Path(name, path)
}
func TestSharedResourceMonitor(t *testing.T) {
// set shorter intervals for testing
var prevInitialDelay, prevMinInterval, prevMaxInterval time.Duration
monitorInitialTickDelay, prevInitialDelay = 0, monitorInitialTickDelay
monitorMaxTickInterval, prevMaxInterval = 100*time.Millisecond, monitorMaxTickInterval
monitorMinTickInterval, prevMinInterval = 10*time.Millisecond, monitorMinTickInterval
t.Cleanup(func() {
monitorInitialTickDelay = prevInitialDelay
monitorMaxTickInterval = prevMaxInterval
monitorMinTickInterval = prevMinInterval
})
testEnvoyPid := 99
tempDir := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(tempDir, ".fs"), 0o777))
testMemoryCurrentPath := filepath.Join(tempDir, ".fs/memory.current")
testMemoryMaxPath := filepath.Join(tempDir, ".fs/memory.max")
updateMemoryCurrent := func(value string) {
t.Log("updating memory.current to", value)
f, err := os.OpenFile(testMemoryCurrentPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
require.NoError(t, err)
f.WriteString(value)
require.NoError(t, f.Close())
}
updateMemoryMax := func(value string) {
t.Log("updating memory.max to", value)
f, err := os.OpenFile(testMemoryMaxPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
require.NoError(t, err)
f.WriteString(value)
require.NoError(t, f.Close())
}
updateMemoryCurrent("100")
updateMemoryMax("200")
driver := &pathOverrideDriver{
CgroupDriver: &cgroupV2Driver{
root: "sys/fs/cgroup",
fs: &hybridTestFS{
base: with(v2Fs, fstest.MapFS{
fmt.Sprintf("proc/%d/cgroup", os.Getpid()): v2Fs["proc/1/cgroup"],
fmt.Sprintf("proc/%d/mountinfo", os.Getpid()): v2Fs["proc/1/mountinfo"],
fmt.Sprintf("proc/%d/cgroup", testEnvoyPid): v2Fs["proc/1/cgroup"],
fmt.Sprintf("proc/%d/mountinfo", testEnvoyPid): v2Fs["proc/1/mountinfo"],
}),
tempDir: tempDir,
},
},
overrides: map[CgroupFilePath]string{
MemoryUsagePath: testMemoryCurrentPath,
MemoryLimitPath: testMemoryMaxPath,
},
}
configSrc := config.NewStaticSource(&config.Config{})
monitor, err := NewSharedResourceMonitor(context.Background(), configSrc, tempDir, WithCgroupDriver(driver))
require.NoError(t, err)
readMemorySaturation := func(t assert.TestingT) string {
f, err := os.ReadFile(filepath.Join(tempDir, "resource_monitor/memory/cgroup_memory_saturation"))
assert.NoError(t, err)
return string(f)
}
assert.Equal(t, "0", readMemorySaturation(t))
ctx, ca := context.WithCancel(context.Background())
errC := make(chan error)
go func() {
defer close(errC)
errC <- monitor.Run(ctx, testEnvoyPid)
}()
timeout := 1 * time.Second
interval := 10 * time.Millisecond
// 100/200
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.Equal(c, "0.500000", readMemorySaturation(c))
}, timeout, interval)
// 150/200
updateMemoryCurrent("150")
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.Equal(c, "0.750000", readMemorySaturation(c))
}, timeout, interval)
// 150/300
updateMemoryMax("300")
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.Equal(c, "0.500000", readMemorySaturation(c))
}, timeout, interval)
// 150/unlimited
updateMemoryMax("max")
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.Equal(c, "0.000000", readMemorySaturation(c))
}, timeout, interval)
// 150/145 (over limit)
updateMemoryMax("145")
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.Equal(c, "1.000000", readMemorySaturation(c))
}, timeout, interval)
// 150/150
updateMemoryMax("150")
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.Equal(c, "1.000000", readMemorySaturation(c))
}, timeout, interval)
configSrc.SetConfig(ctx, &config.Config{
Options: &config.Options{
RuntimeFlags: config.RuntimeFlags{
config.RuntimeFlagEnvoyResourceManagerEnabled: false,
},
},
})
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.Equal(c, "0.000000", readMemorySaturation(c))
}, timeout, interval)
configSrc.SetConfig(ctx, &config.Config{
Options: &config.Options{
RuntimeFlags: config.RuntimeFlags{
config.RuntimeFlagEnvoyResourceManagerEnabled: true,
},
},
})
updateMemoryMax("150")
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.Equal(c, "1.000000", readMemorySaturation(c))
}, timeout, interval)
ca()
assert.ErrorIs(t, <-errC, context.Canceled)
// test deletion of memory.max
updateMemoryCurrent("150")
updateMemoryMax("300")
monitor, err = NewSharedResourceMonitor(context.Background(), configSrc, tempDir, WithCgroupDriver(driver))
require.NoError(t, err)
errC = make(chan error)
go func() {
defer close(errC)
errC <- monitor.Run(context.Background(), testEnvoyPid)
}()
// 150/300
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.Equal(c, "0.500000", readMemorySaturation(c))
}, timeout, interval)
require.NoError(t, os.Remove(testMemoryMaxPath))
assert.EqualError(t, <-errC, "memory limit watcher stopped")
}
func TestBootstrapConfig(t *testing.T) {
b := envoyconfig.New("localhost:1111", "localhost:2222", "localhost:3333", filemgr.NewManager(), nil)
testEnvoyPid := 99
tempDir := t.TempDir()
monitor, err := NewSharedResourceMonitor(context.Background(), config.NewStaticSource(nil), tempDir, WithCgroupDriver(&cgroupV2Driver{
root: "sys/fs/cgroup",
fs: &hybridTestFS{
base: with(v2Fs, fstest.MapFS{
fmt.Sprintf("proc/%d/cgroup", os.Getpid()): v2Fs["proc/1/cgroup"],
fmt.Sprintf("proc/%d/mountinfo", os.Getpid()): v2Fs["proc/1/mountinfo"],
fmt.Sprintf("proc/%d/cgroup", testEnvoyPid): v2Fs["proc/1/cgroup"],
fmt.Sprintf("proc/%d/mountinfo", testEnvoyPid): v2Fs["proc/1/mountinfo"],
}),
tempDir: tempDir,
},
}))
require.NoError(t, err)
bootstrap, err := b.BuildBootstrap(context.Background(), &config.Config{
Options: &config.Options{
EnvoyAdminAddress: "localhost:9901",
},
}, false)
assert.NoError(t, err)
monitor.ApplyBootstrapConfig(bootstrap)
testutil.AssertProtoJSONEqual(t, fmt.Sprintf(`
{
"actions": [
{
"name": "envoy.overload_actions.shrink_heap",
"triggers": [
{
"name": "envoy.resource_monitors.injected_resource",
"threshold": {
"value": 0.9
}
}
]
},
{
"name": "envoy.overload_actions.reduce_timeouts",
"triggers": [
{
"name": "envoy.resource_monitors.injected_resource",
"scaled": {
"saturationThreshold": 0.95,
"scalingThreshold": 0.85
}
}
],
"typedConfig": {
"@type": "type.googleapis.com/envoy.config.overload.v3.ScaleTimersOverloadActionConfig",
"timerScaleFactors": [
{
"minScale": {
"value": 50
},
"timer": "HTTP_DOWNSTREAM_CONNECTION_IDLE"
}
]
}
},
{
"name": "envoy.overload_actions.reset_high_memory_stream",
"triggers": [
{
"name": "envoy.resource_monitors.injected_resource",
"scaled": {
"saturationThreshold": 0.98,
"scalingThreshold": 0.9
}
}
]
},
{
"name": "envoy.overload_actions.stop_accepting_connections",
"triggers": [
{
"name": "envoy.resource_monitors.injected_resource",
"threshold": {
"value": 0.95
}
}
]
},
{
"name": "envoy.overload_actions.disable_http_keepalive",
"triggers": [
{
"name": "envoy.resource_monitors.injected_resource",
"threshold": {
"value": 0.98
}
}
]
},
{
"name": "envoy.overload_actions.stop_accepting_requests",
"triggers": [
{
"name": "envoy.resource_monitors.injected_resource",
"threshold": {
"value": 0.99
}
}
]
}
],
"bufferFactoryConfig": {
"minimumAccountToTrackPowerOfTwo": 20
},
"resourceMonitors": [
{
"name": "envoy.resource_monitors.global_downstream_max_connections",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.resource_monitors.downstream_connections.v3.DownstreamConnectionsConfig",
"maxActiveDownstreamConnections": "50000"
}
},
{
"name": "envoy.resource_monitors.injected_resource",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.resource_monitors.injected_resource.v3.InjectedResourceConfig",
"filename": "%s/resource_monitor/memory/cgroup_memory_saturation"
}
}
]
}
`, tempDir), bootstrap.OverloadManager)
}
func TestComputeScaledTickInterval(t *testing.T) {
cases := []struct {
saturation float64
expected time.Duration
}{
0: {
saturation: 0.0,
expected: 10000 * time.Millisecond,
},
1: {
saturation: 1.0,
expected: 250 * time.Millisecond,
},
2: {
saturation: 0.5,
expected: 5125 * time.Millisecond,
},
3: {
// duration should round to the nearest millisecond
saturation: 0.3333,
expected: 6750 * time.Millisecond,
},
4: {
saturation: -1.0,
expected: 10000 * time.Millisecond,
},
5: {
// saturation > 1 should be clamped to 1
saturation: 1.5,
expected: 250 * time.Millisecond,
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
assert.Equal(t, c.expected, computeScaledTickInterval(c.saturation))
})
}
}