Skip to content

Commit

Permalink
Merge pull request containerd#98 from mariomac/ignoremem
Browse files Browse the repository at this point in the history
Allows ignoring memory modules
  • Loading branch information
estesp committed Sep 23, 2019
2 parents 484059a + ad1c4b9 commit abd0b19
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 4 deletions.
40 changes: 36 additions & 4 deletions memory.go
Expand Up @@ -32,14 +32,43 @@ import (
"golang.org/x/sys/unix"
)

func NewMemory(root string) *memoryController {
return &memoryController{
root: filepath.Join(root, string(Memory)),
// NewMemory returns a Memory controller given the root folder of cgroups.
// It may optionally accept other configuration options, such as IgnoreModules(...)
func NewMemory(root string, options ...func(*memoryController)) *memoryController {
mc := &memoryController{
root: filepath.Join(root, string(Memory)),
ignored: map[string]struct{}{},
}
for _, opt := range options {
opt(mc)
}
return mc
}

// IgnoreModules configure the memory controller to not read memory metrics for some
// module names (e.g. passing "memsw" would avoid all the memory.memsw.* entries)
func IgnoreModules(names ...string) func(*memoryController) {
return func(mc *memoryController) {
for _, name := range names {
mc.ignored[name] = struct{}{}
}
}
}

// OptionalSwap allows the memory controller to not fail if cgroups is not accounting
// Swap memory (there are no memory.memsw.* entries)
func OptionalSwap() func(*memoryController) {
return func(mc *memoryController) {
_, err := os.Stat(filepath.Join(mc.root, "memory.memsw.usage_in_bytes"))
if os.IsNotExist(err) {
mc.ignored["memsw"] = struct{}{}
}
}
}

type memoryController struct {
root string
root string
ignored map[string]struct{}
}

func (m *memoryController) Name() Name {
Expand Down Expand Up @@ -133,6 +162,9 @@ func (m *memoryController) Stat(path string, stats *v1.Metrics) error {
entry: stats.Memory.KernelTCP,
},
} {
if _, ok := m.ignored[t.module]; ok {
continue
}
for _, tt := range []struct {
name string
value *uint64
Expand Down
155 changes: 155 additions & 0 deletions memory_test.go
Expand Up @@ -17,6 +17,10 @@
package cgroups

import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"testing"

Expand Down Expand Up @@ -106,3 +110,154 @@ func TestParseMemoryStats(t *testing.T) {
}
}
}

func TestMemoryController_Stat(t *testing.T) {
// GIVEN a cgroups folder with all the memory metrics
modules := []string{"", "memsw", "kmem", "kmem.tcp"}
metrics := []string{"usage_in_bytes", "max_usage_in_bytes", "failcnt", "limit_in_bytes"}
tmpRoot := buildMemoryMetrics(t, modules, metrics)

// WHEN the memory controller reads the metrics stats
mc := NewMemory(tmpRoot)
stats := v1.Metrics{}
if err := mc.Stat("", &stats); err != nil {
t.Errorf("can't get stats: %v", err)
}

// THEN all the memory stats have been completely loaded in memory
checkMemoryStatIsComplete(t, stats.Memory)
}

func TestMemoryController_Stat_IgnoreModules(t *testing.T) {
// GIVEN a cgroups folder that accounts for all the metrics BUT swap memory
modules := []string{"", "kmem", "kmem.tcp"}
metrics := []string{"usage_in_bytes", "max_usage_in_bytes", "failcnt", "limit_in_bytes"}
tmpRoot := buildMemoryMetrics(t, modules, metrics)

// WHEN the memory controller explicitly ignores memsw module and reads the data
mc := NewMemory(tmpRoot, IgnoreModules("memsw"))
stats := v1.Metrics{}
if err := mc.Stat("", &stats); err != nil {
t.Errorf("can't get stats: %v", err)
}

// THEN the swap memory stats are not loaded but all the other memory metrics are
checkMemoryStatHasNoSwap(t, stats.Memory)
}

func TestMemoryController_Stat_OptionalSwap_HasSwap(t *testing.T) {
// GIVEN a cgroups folder with all the memory metrics
modules := []string{"", "memsw", "kmem", "kmem.tcp"}
metrics := []string{"usage_in_bytes", "max_usage_in_bytes", "failcnt", "limit_in_bytes"}
tmpRoot := buildMemoryMetrics(t, modules, metrics)

// WHEN a memory controller that ignores swap only if it is missing reads stats
mc := NewMemory(tmpRoot, OptionalSwap())
stats := v1.Metrics{}
if err := mc.Stat("", &stats); err != nil {
t.Errorf("can't get stats: %v", err)
}

// THEN all the memory stats have been completely loaded in memory
checkMemoryStatIsComplete(t, stats.Memory)
}

func TestMemoryController_Stat_OptionalSwap_NoSwap(t *testing.T) {
// GIVEN a cgroups folder that accounts for all the metrics BUT swap memory
modules := []string{"", "kmem", "kmem.tcp"}
metrics := []string{"usage_in_bytes", "max_usage_in_bytes", "failcnt", "limit_in_bytes"}
tmpRoot := buildMemoryMetrics(t, modules, metrics)

// WHEN a memory controller that ignores swap only if it is missing reads stats
mc := NewMemory(tmpRoot, OptionalSwap())
stats := v1.Metrics{}
if err := mc.Stat("", &stats); err != nil {
t.Errorf("can't get stats: %v", err)
}

// THEN the swap memory stats are not loaded but all the other memory metrics are
checkMemoryStatHasNoSwap(t, stats.Memory)
}

func checkMemoryStatIsComplete(t *testing.T, mem *v1.MemoryStat) {
index := []uint64{
mem.Usage.Usage,
mem.Usage.Max,
mem.Usage.Failcnt,
mem.Usage.Limit,
mem.Swap.Usage,
mem.Swap.Max,
mem.Swap.Failcnt,
mem.Swap.Limit,
mem.Kernel.Usage,
mem.Kernel.Max,
mem.Kernel.Failcnt,
mem.Kernel.Limit,
mem.KernelTCP.Usage,
mem.KernelTCP.Max,
mem.KernelTCP.Failcnt,
mem.KernelTCP.Limit,
}
for i, v := range index {
if v != uint64(i) {
t.Errorf("expected value at index %d to be %d but received %d", i, i, v)
}
}
}

func checkMemoryStatHasNoSwap(t *testing.T, mem *v1.MemoryStat) {
if mem.Swap.Usage != 0 || mem.Swap.Limit != 0 ||
mem.Swap.Max != 0 || mem.Swap.Failcnt != 0 {
t.Errorf("swap memory should have been ignored. Got: %+v", mem.Swap)
}
index := []uint64{
mem.Usage.Usage,
mem.Usage.Max,
mem.Usage.Failcnt,
mem.Usage.Limit,
mem.Kernel.Usage,
mem.Kernel.Max,
mem.Kernel.Failcnt,
mem.Kernel.Limit,
mem.KernelTCP.Usage,
mem.KernelTCP.Max,
mem.KernelTCP.Failcnt,
mem.KernelTCP.Limit,
}
for i, v := range index {
if v != uint64(i) {
t.Errorf("expected value at index %d to be %d but received %d", i, i, v)
}
}
}

// buildMemoryMetrics creates fake cgroups memory entries in a temporary dir. Returns the fake cgroups root
func buildMemoryMetrics(t *testing.T, modules []string, metrics []string) string {
tmpRoot, err := ioutil.TempDir("", "memtests")
if err != nil {
t.Fatal(err)
}
tmpDir := path.Join(tmpRoot, string(Memory))
if err := os.MkdirAll(tmpDir, defaultDirPerm); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(path.Join(tmpDir, "memory.stat"), []byte(memoryData), defaultFilePerm); err != nil {
t.Fatal(err)
}
cnt := 0
for _, mod := range modules {
for _, metric := range metrics {
var fileName string
if mod == "" {
fileName = path.Join(tmpDir, strings.Join([]string{"memory", metric}, "."))
} else {
fileName = path.Join(tmpDir, strings.Join([]string{"memory", mod, metric}, "."))
}
if err := ioutil.WriteFile(fileName, []byte(fmt.Sprintln(cnt)), defaultFilePerm); err != nil {
t.Fatal(err)
}
cnt++
}
}
return tmpRoot
}

0 comments on commit abd0b19

Please sign in to comment.