From b5e3683d80677a70b704c95b79eeb58136d617b1 Mon Sep 17 00:00:00 2001 From: Geon Kim Date: Fri, 14 Mar 2025 12:12:01 +0900 Subject: [PATCH 1/6] chore(examples/gosigar): remove gosigar example --- examples/gosigar/go.mod | 22 ---------------------- examples/gosigar/go.sum | 34 ---------------------------------- examples/gosigar/main.go | 25 ------------------------- 3 files changed, 81 deletions(-) delete mode 100644 examples/gosigar/go.mod delete mode 100644 examples/gosigar/go.sum delete mode 100644 examples/gosigar/main.go diff --git a/examples/gosigar/go.mod b/examples/gosigar/go.mod deleted file mode 100644 index de653c7..0000000 --- a/examples/gosigar/go.mod +++ /dev/null @@ -1,22 +0,0 @@ -module github.com/KimMachineGun/automemlimit/examples/gosigar - -go 1.22.0 - -toolchain go1.23.3 - -require ( - github.com/KimMachineGun/automemlimit v0.0.0 - github.com/cloudfoundry/gosigar v1.3.30 -) - -require ( - github.com/google/go-cmp v0.6.0 // indirect - github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/stretchr/testify v1.8.4 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/tools v0.27.0 // indirect -) - -replace github.com/KimMachineGun/automemlimit => ../../ diff --git a/examples/gosigar/go.sum b/examples/gosigar/go.sum deleted file mode 100644 index d093f80..0000000 --- a/examples/gosigar/go.sum +++ /dev/null @@ -1,34 +0,0 @@ -github.com/cloudfoundry/gosigar v1.3.30 h1:ZQPPt8RY72T8V+OZqPAi1qzkqH6UPhrAY8lfmDklNuI= -github.com/cloudfoundry/gosigar v1.3.30/go.mod h1:v1aji1eOWmI6/v9T9Gd9ef1a2FEi9m9/25UnfHO0org= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ= -github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA= -github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= -golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/gosigar/main.go b/examples/gosigar/main.go deleted file mode 100644 index 9c70bd5..0000000 --- a/examples/gosigar/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "github.com/KimMachineGun/automemlimit/memlimit" - sigar "github.com/cloudfoundry/gosigar" -) - -func init() { - memlimit.SetGoMemLimitWithOpts( - memlimit.WithProvider( - memlimit.ApplyFallback( - memlimit.FromCgroup, - FromGoSigar, - ), - ), - ) -} - -func main() {} - -func FromGoSigar() (uint64, error) { - var mem sigar.Mem - err := mem.Get() - return mem.Total, err -} From a659ed11b8c2b268723cc09710e52a4bddfcf5bb Mon Sep 17 00:00:00 2001 From: Geon Kim Date: Thu, 24 Apr 2025 10:54:42 +0900 Subject: [PATCH 2/6] fix(memlimit): fix mountinfo validation logic when super options have spaces --- memlimit/cgroups.go | 4 +--- memlimit/cgroups_test.go | 15 ++++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/memlimit/cgroups.go b/memlimit/cgroups.go index 73a57c3..69d1771 100644 --- a/memlimit/cgroups.go +++ b/memlimit/cgroups.go @@ -276,11 +276,9 @@ func parseMountInfoLine(line string) (mountInfo, error) { fields1 = append(fields1, "") } - fields2 := strings.Split(fieldss[1], " ") + fields2 := strings.SplitN(fieldss[1], " ", 3) if len(fields2) < 3 { return mountInfo{}, fmt.Errorf("not enough fields after separator: %v", fields2) - } else if len(fields2) > 3 { - return mountInfo{}, fmt.Errorf("too many fields after separator: %v", fields2) } return mountInfo{ diff --git a/memlimit/cgroups_test.go b/memlimit/cgroups_test.go index 4368de8..8b9dcda 100644 --- a/memlimit/cgroups_test.go +++ b/memlimit/cgroups_test.go @@ -57,11 +57,6 @@ func TestParseMountInfoLine(t *testing.T) { input: "36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3", wantErr: `not enough fields after separator: [ext3]`, }, - { - name: "too many fields on right side", - input: "36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw extra", - wantErr: `too many fields after separator: [ext3 /dev/root rw extra]`, - }, { name: "empty line", input: "", @@ -87,6 +82,16 @@ func TestParseMountInfoLine(t *testing.T) { SuperOptions: "rw,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota", }, }, + { + name: "super options have spaces (issue #28)", + input: `1391 1160 0:151 / /Docker/host rw,noatime - 9p C:\134Program\040Files\134Docker\134Docker\134resources rw,dirsync,aname=drvfs;path=C:\Program Files\Docker\Docker\resources;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=3,wfd=3`, + want: mountInfo{ + Root: "/", + MountPoint: "/Docker/host", + FilesystemType: "9p", + SuperOptions: `rw,dirsync,aname=drvfs;path=C:\Program Files\Docker\Docker\resources;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=3,wfd=3`, + }, + }, } for _, tt := range tests { From a9a712bee9977065cf72b4f29fcaf3a4e0573e13 Mon Sep 17 00:00:00 2001 From: Geon Kim Date: Mon, 28 Apr 2025 10:57:15 +0900 Subject: [PATCH 3/6] ci: bump ubuntu version --- .github/workflows/test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c019a7b..86b7311 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,8 +3,8 @@ name: Test on: [ push, pull_request ] jobs: - test-ubuntu-20_04: - runs-on: ubuntu-20.04 + test-ubuntu-22_04: + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 @@ -19,18 +19,18 @@ jobs: - name: Run tests in Go container (1000m) run: | - docker run --rm -v=$(pwd):/app -w=/app -m=1000m golang:1.22 go test -v ./... -expected=1048576000 -cgroup-version 1 + docker run --rm -v=$(pwd):/app -w=/app -m=1000m golang:1.22 go test -v ./... -expected=1048576000 -cgroup-version 2 - name: Run tests in Go container (4321m) run: | - docker run --rm -v=$(pwd):/app -w=/app -m=4321m golang:1.22 go test -v ./... -expected=4530896896 -cgroup-version 1 + docker run --rm -v=$(pwd):/app -w=/app -m=4321m golang:1.22 go test -v ./... -expected=4530896896 -cgroup-version 2 - name: Run tests in Go container (system memory limit) run: | - docker run --rm -v=$(pwd):/app -w=/app golang:1.22 go test -v ./... -expected-system=$(($(awk '/MemTotal/ {print $2}' /proc/meminfo) * 1024)) -cgroup-version 1 + docker run --rm -v=$(pwd):/app -w=/app golang:1.22 go test -v ./... -expected-system=$(($(awk '/MemTotal/ {print $2}' /proc/meminfo) * 1024)) -cgroup-version 2 - test-ubuntu-22_04: - runs-on: ubuntu-22.04 + test-ubuntu-24_04: + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 From b2c01e82ab357d10c79031cef7b2704f225798f1 Mon Sep 17 00:00:00 2001 From: Geon Kim Date: Sat, 14 Jun 2025 22:48:39 +0900 Subject: [PATCH 4/6] fix(memlimit): move goroutine execution inside refresh function --- memlimit/memlimit.go | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/memlimit/memlimit.go b/memlimit/memlimit.go index cbd53ce..b23980a 100644 --- a/memlimit/memlimit.go +++ b/memlimit/memlimit.go @@ -169,7 +169,7 @@ func SetGoMemLimitWithOpts(opts ...Option) (_ int64, _err error) { // set the memory limit and start refresh limit, err := updateGoMemLimit(uint64(snapshot), provider, cfg.logger) - go refresh(provider, cfg.logger, cfg.refresh) + refresh(provider, cfg.logger, cfg.refresh) if err != nil { if errors.Is(err, ErrNoLimit) { cfg.logger.Info("memory is not limited, skipping") @@ -200,7 +200,7 @@ func updateGoMemLimit(currLimit uint64, provider Provider, logger *slog.Logger) return newLimit, nil } -// refresh periodically fetches the memory limit from the provider and reapplies it if it has changed. +// refresh spawns a goroutine that runs every refresh duration and updates the GOMEMLIMIT if it has changed. // See more details in the documentation of WithRefreshInterval. func refresh(provider Provider, logger *slog.Logger, refresh time.Duration) { if refresh == 0 { @@ -210,22 +210,24 @@ func refresh(provider Provider, logger *slog.Logger, refresh time.Duration) { provider = noErrNoLimitProvider(provider) t := time.NewTicker(refresh) - for range t.C { - err := func() (_err error) { - snapshot := debug.SetMemoryLimit(-1) - defer rollbackOnPanic(logger, snapshot, &_err) - - _, err := updateGoMemLimit(uint64(snapshot), provider, logger) + go func() { + for range t.C { + err := func() (_err error) { + snapshot := debug.SetMemoryLimit(-1) + defer rollbackOnPanic(logger, snapshot, &_err) + + _, err := updateGoMemLimit(uint64(snapshot), provider, logger) + if err != nil { + return err + } + + return nil + }() if err != nil { - return err + logger.Error("failed to refresh GOMEMLIMIT", slog.Any("error", err)) } - - return nil - }() - if err != nil { - logger.Error("failed to refresh GOMEMLIMIT", slog.Any("error", err)) } - } + }() } // rollbackOnPanic rollbacks to the snapshot on panic. From e8d01358c6886d609da9e71d6b805c31770fb394 Mon Sep 17 00:00:00 2001 From: Sergey <48452323+sergeysedoy97@users.noreply.github.com> Date: Thu, 10 Jul 2025 05:38:34 +0300 Subject: [PATCH 5/6] fix(memlimit): use memory.stat instead of memory.stats (#30) --- memlimit/cgroups.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/memlimit/cgroups.go b/memlimit/cgroups.go index 69d1771..81559e3 100644 --- a/memlimit/cgroups.go +++ b/memlimit/cgroups.go @@ -157,7 +157,7 @@ func getMemoryLimitV1(chs []cgroupHierarchy, mis []mountInfo) (uint64, error) { return 0, err } - // retrieve the memory limit from the memory.stats and memory.limit_in_bytes files. + // retrieve the memory limit from the memory.stat and memory.limit_in_bytes files. return readMemoryLimitV1FromPath(cgroupPath) } @@ -173,7 +173,7 @@ func getCgroupV1NoLimit() uint64 { func readMemoryLimitV1FromPath(cgroupPath string) (uint64, error) { // read hierarchical_memory_limit and memory.limit_in_bytes files. // but if hierarchical_memory_limit is not available, then use the max value as a fallback. - hml, err := readHierarchicalMemoryLimit(filepath.Join(cgroupPath, "memory.stats")) + hml, err := readHierarchicalMemoryLimit(filepath.Join(cgroupPath, "memory.stat")) if err != nil && !errors.Is(err, os.ErrNotExist) { return 0, fmt.Errorf("failed to read hierarchical_memory_limit: %w", err) } else if hml == 0 { @@ -202,8 +202,8 @@ func readMemoryLimitV1FromPath(cgroupPath string) (uint64, error) { return limit, nil } -// readHierarchicalMemoryLimit extracts hierarchical_memory_limit from memory.stats. -// this function expects the path to be memory.stats file. +// readHierarchicalMemoryLimit extracts hierarchical_memory_limit from memory.stat. +// this function expects the path to be memory.stat file. func readHierarchicalMemoryLimit(path string) (uint64, error) { file, err := os.Open(path) if err != nil { @@ -217,12 +217,12 @@ func readHierarchicalMemoryLimit(path string) (uint64, error) { fields := strings.Split(line, " ") if len(fields) < 2 { - return 0, fmt.Errorf("failed to parse memory.stats %q: not enough fields", line) + return 0, fmt.Errorf("failed to parse memory.stat %q: not enough fields", line) } if fields[0] == "hierarchical_memory_limit" { if len(fields) > 2 { - return 0, fmt.Errorf("failed to parse memory.stats %q: too many fields for hierarchical_memory_limit", line) + return 0, fmt.Errorf("failed to parse memory.stat %q: too many fields for hierarchical_memory_limit", line) } return strconv.ParseUint(fields[1], 10, 64) } From 6d12049dcf3e429d3aeab23c3dd57ee4ce610292 Mon Sep 17 00:00:00 2001 From: Geon Kim Date: Wed, 8 Oct 2025 14:39:30 +0900 Subject: [PATCH 6/6] fix(memlimit): respect parent cgroup limits in v2 (#31) --- memlimit/cgroups.go | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/memlimit/cgroups.go b/memlimit/cgroups.go index 81559e3..2f0a840 100644 --- a/memlimit/cgroups.go +++ b/memlimit/cgroups.go @@ -103,8 +103,8 @@ func getMemoryLimitV2(chs []cgroupHierarchy, mis []mountInfo) (uint64, error) { return 0, err } - // retrieve the memory limit from the memory.max file - return readMemoryLimitV2FromPath(filepath.Join(cgroupPath, "memory.max")) + // retrieve the memory limit from the memory.max recursively. + return walkCgroupV2Hierarchy(cgroupPath, mountPoint) } // readMemoryLimitV2FromPath reads the memory limit for cgroup v2 from the given path. @@ -131,6 +131,39 @@ func readMemoryLimitV2FromPath(path string) (uint64, error) { return limit, nil } +// walkCgroupV2Hierarchy walks up the cgroup v2 hierarchy to find the most restrictive memory limit. +func walkCgroupV2Hierarchy(cgroupPath, mountPoint string) (uint64, error) { + var ( + found = false + minLimit uint64 = math.MaxUint64 + currentPath = cgroupPath + ) + for { + limit, err := readMemoryLimitV2FromPath(filepath.Join(currentPath, "memory.max")) + if err != nil && !errors.Is(err, ErrNoLimit) { + return 0, err + } else if err == nil { + found = true + minLimit = min(minLimit, limit) + } + + if currentPath == mountPoint { + break + } + + parent := filepath.Dir(currentPath) + if parent == currentPath { + break + } + currentPath = parent + } + if !found { + return 0, ErrNoLimit + } + + return minLimit, nil +} + // getMemoryLimitV1 retrieves the memory limit from the cgroup v1 controller. func getMemoryLimitV1(chs []cgroupHierarchy, mis []mountInfo) (uint64, error) { // find the cgroup v1 path for the memory controller.