Skip to content

Commit f056ce4

Browse files
committed
feat!: simplify API surface for v1
1 parent 6d12049 commit f056ce4

21 files changed

Lines changed: 877 additions & 972 deletions

README.md

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@ See more details about `GOMEMLIMIT` [here](https://tip.golang.org/doc/gc-guide#M
1010

1111
## Notice
1212

13-
Version `v0.5.0` introduces a fallback to system memory limits as an experimental feature when cgroup limits are unavailable. Activate this by setting `AUTOMEMLIMIT_EXPERIMENT=system`.
14-
You can also use system memory limits via `memlimit.FromSystem` provider directly.
13+
Version `v1.0.0` introduces breaking changes to improve usability by simplifying multiple functions into a single `memlimit.Set()` function with options, and to provide more consistent and predictable behavior.
1514

16-
This feature is under evaluation and might become a default or be removed based on user feedback.
17-
If you have any feedback about this feature, please open an issue.
15+
Key changes from v0.x:
16+
- `SetGoMemLimitWithOpts`, `SetGoMemLimit`, `SetGoMemLimitWithProvider`, `SetGoMemLimitWithEnv` removed; use `Set` instead
17+
- `FromCgroupV1`, `FromCgroupV2`, `FromCgroupHybrid` removed; use `FromCgroup` instead
18+
- `AUTOMEMLIMIT_EXPERIMENT=system` removed; use `memlimit.ApplyFallback(memlimit.FromCgroup, memlimit.FromSystem)` for explicit fallback
19+
- `AUTOMEMLIMIT_DEBUG` removed; use `memlimit.WithLogger(slog.Default())` instead
20+
- `import _ "github.com/KimMachineGun/automemlimit"` now automatically falls back to system memory when cgroup is unavailable or limit is not set (ErrNoLimit). Note: `memlimit.Set()` does NOT fall back automatically; use `memlimit.ApplyFallback` explicitly.
21+
- When `ErrNoLimit` is returned from the provider, GOMEMLIMIT is set to `math.MaxInt64` (unlimited) and returned as a successful result
22+
- `Set` now returns the previous GOMEMLIMIT value instead of 0 when skipping or on error
23+
- `WithRefreshInterval(ctx, duration)` now requires a `context.Context` to control the refresh goroutine
24+
- When refresh is enabled, provider errors are logged and ignored; skipping does not start refresh
25+
- New `WithMin(bytes)` option to set a minimum memory limit
1826

1927
## Installation
2028

@@ -27,41 +35,39 @@ go get github.com/KimMachineGun/automemlimit@latest
2735
```go
2836
package main
2937

30-
// By default, it sets `GOMEMLIMIT` to 90% of cgroup's memory limit.
31-
// This is equivalent to `memlimit.SetGoMemLimitWithOpts(memlimit.WithLogger(slog.Default()))`
32-
// To disable logging, use `memlimit.SetGoMemLimitWithOpts` directly.
38+
// Sets GOMEMLIMIT to 90% of cgroup's memory limit.
39+
// Falls back to system memory if cgroup is unavailable or limit is not set (ErrNoLimit).
3340
import _ "github.com/KimMachineGun/automemlimit"
3441
```
3542

36-
or
43+
> **Note:** `memlimit.Set()` defaults to `memlimit.FromCgroup` only. Use `memlimit.ApplyFallback(memlimit.FromCgroup, memlimit.FromSystem)` explicitly if you need system memory fallback.
44+
45+
For more control, use `memlimit.Set()` directly:
3746

3847
```go
3948
package main
4049

41-
import "github.com/KimMachineGun/automemlimit/memlimit"
50+
import (
51+
"context"
52+
"log/slog"
53+
"time"
4254

43-
func init() {
44-
memlimit.SetGoMemLimitWithOpts(
45-
memlimit.WithRatio(0.9),
46-
memlimit.WithProvider(memlimit.FromCgroup),
47-
memlimit.WithLogger(slog.Default()),
48-
memlimit.WithRefreshInterval(1*time.Minute),
49-
)
50-
memlimit.SetGoMemLimitWithOpts(
51-
memlimit.WithRatio(0.9),
55+
"github.com/KimMachineGun/automemlimit/memlimit"
56+
)
57+
58+
func main() {
59+
ctx, cancel := context.WithCancel(context.Background())
60+
defer cancel()
61+
62+
memlimit.Set(
63+
// Fallback to system memory if cgroup is unavailable or limit is not set (ErrNoLimit).
5264
memlimit.WithProvider(
53-
memlimit.ApplyFallback(
54-
memlimit.FromCgroup,
55-
memlimit.FromSystem,
56-
),
65+
memlimit.ApplyFallback(memlimit.FromCgroup, memlimit.FromSystem),
5766
),
58-
memlimit.WithRefreshInterval(1*time.Minute),
67+
memlimit.WithRatio(0.9), // Use 90% of the limit (default)
68+
memlimit.WithMin(512 * 1024 * 1024), // At least 512MB
69+
memlimit.WithRefreshInterval(ctx, time.Minute), // Refresh every minute
70+
memlimit.WithLogger(slog.Default()),
5971
)
60-
memlimit.SetGoMemLimit(0.9)
61-
memlimit.SetGoMemLimitWithProvider(memlimit.Limit(1024*1024), 0.9)
62-
memlimit.SetGoMemLimitWithProvider(memlimit.FromCgroup, 0.9)
63-
memlimit.SetGoMemLimitWithProvider(memlimit.FromCgroupV1, 0.9)
64-
memlimit.SetGoMemLimitWithProvider(memlimit.FromCgroupHybrid, 0.9)
65-
memlimit.SetGoMemLimitWithProvider(memlimit.FromCgroupV2, 0.9)
6672
}
6773
```

automemlimit.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
// Package automemlimit provides automatic GOMEMLIMIT configuration.
2+
//
3+
// Importing this package automatically sets GOMEMLIMIT based on cgroup memory limits,
4+
// falling back to system memory if cgroup is unavailable or limit is not set ([memlimit.ErrNoLimit]).
5+
//
6+
// import _ "github.com/KimMachineGun/automemlimit"
7+
//
8+
// For more control, use the memlimit package directly.
19
package automemlimit
210

311
import (
@@ -7,7 +15,13 @@ import (
715
)
816

917
func init() {
10-
memlimit.SetGoMemLimitWithOpts(
18+
memlimit.Set(
19+
memlimit.WithProvider(
20+
memlimit.ApplyFallback(
21+
memlimit.FromCgroup,
22+
memlimit.FromSystem,
23+
),
24+
),
1125
memlimit.WithLogger(slog.Default()),
1226
)
1327
}

examples/dynamic/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bytes"
5+
"context"
56
"errors"
67
"log/slog"
78
"os"
@@ -15,11 +16,11 @@ import (
1516
func init() {
1617
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
1718

18-
memlimit.SetGoMemLimitWithOpts(
19+
memlimit.Set(
1920
memlimit.WithProvider(
2021
FileProvider("limit.txt"),
2122
),
22-
memlimit.WithRefreshInterval(5*time.Second),
23+
memlimit.WithRefreshInterval(context.Background(), 5*time.Second),
2324
memlimit.WithLogger(slog.Default()),
2425
)
2526
}

examples/logger/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
func init() {
11-
memlimit.SetGoMemLimitWithOpts(
11+
memlimit.Set(
1212
memlimit.WithProvider(
1313
memlimit.Limit(1024*1024*1024),
1414
),

examples/system/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
)
66

77
func init() {
8-
memlimit.SetGoMemLimitWithOpts(
8+
memlimit.Set(
99
memlimit.WithProvider(
1010
memlimit.ApplyFallback(
1111
memlimit.FromCgroup,

memlimit/cgroups.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,11 @@ func readMemoryLimitV1FromPath(cgroupPath string) (uint64, error) {
214214
}
215215

216216
// read memory.limit_in_bytes file.
217-
b, err := os.ReadFile(filepath.Join(cgroupPath, "memory.limit_in_bytes"))
217+
lib, err := readMemoryLimitInBytes(filepath.Join(cgroupPath, "memory.limit_in_bytes"))
218218
if err != nil && !errors.Is(err, os.ErrNotExist) {
219219
return 0, fmt.Errorf("failed to read memory.limit_in_bytes: %w", err)
220-
}
221-
lib, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64)
222-
if err != nil {
223-
return 0, fmt.Errorf("failed to parse memory.limit_in_bytes value: %w", err)
224220
} else if lib == 0 {
225-
hml = math.MaxUint64
221+
lib = math.MaxUint64
226222
}
227223

228224
// use the minimum value between hierarchical_memory_limit and memory.limit_in_bytes.
@@ -267,6 +263,16 @@ func readHierarchicalMemoryLimit(path string) (uint64, error) {
267263
return 0, nil
268264
}
269265

266+
// readMemoryLimitInBytes reads the memory.limit_in_bytes file.
267+
// this function expects the path to be memory.limit_in_bytes file.
268+
func readMemoryLimitInBytes(path string) (uint64, error) {
269+
b, err := os.ReadFile(path)
270+
if err != nil {
271+
return 0, err
272+
}
273+
return strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64)
274+
}
275+
270276
// https://www.man7.org/linux/man-pages/man5/proc_pid_mountinfo.5.html
271277
// 731 771 0:59 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw
272278
//

memlimit/cgroups_linux.go

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,3 @@ package memlimit
77
func FromCgroup() (uint64, error) {
88
return fromCgroup(detectCgroupVersion)
99
}
10-
11-
// FromCgroupV1 retrieves the memory limit from the cgroup v1 controller.
12-
// After v1.0.0, this function could be removed and FromCgroup should be used instead.
13-
func FromCgroupV1() (uint64, error) {
14-
return fromCgroup(func(_ []mountInfo) (bool, bool) {
15-
return true, false
16-
})
17-
}
18-
19-
// FromCgroupHybrid retrieves the memory limit from the cgroup v2 and v1 controller sequentially,
20-
// basically, it is equivalent to FromCgroup.
21-
// After v1.0.0, this function could be removed and FromCgroup should be used instead.
22-
func FromCgroupHybrid() (uint64, error) {
23-
return FromCgroup()
24-
}
25-
26-
// FromCgroupV2 retrieves the memory limit from the cgroup v2 controller.
27-
// After v1.0.0, this function could be removed and FromCgroup should be used instead.
28-
func FromCgroupV2() (uint64, error) {
29-
return fromCgroup(func(_ []mountInfo) (bool, bool) {
30-
return false, true
31-
})
32-
}

memlimit/cgroups_linux_test.go

Lines changed: 8 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package memlimit
55

66
import (
7+
"errors"
78
"testing"
89
)
910

@@ -13,58 +14,16 @@ func TestFromCgroup(t *testing.T) {
1314
}
1415

1516
limit, err := FromCgroup()
16-
if cgVersion == 0 && err != ErrNoCgroup {
17-
t.Fatalf("FromCgroup() error = %v, wantErr %v", err, ErrNoCgroup)
17+
if cgVersion == 0 {
18+
if !errors.Is(err, ErrNoCgroup) {
19+
t.Errorf("FromCgroup() error = %v, want %v", err, ErrNoCgroup)
20+
}
21+
return
1822
}
19-
20-
if err != nil {
21-
t.Fatalf("FromCgroup() error = %v, wantErr %v", err, nil)
22-
}
23-
if limit != expected {
24-
t.Fatalf("FromCgroup() got = %v, want %v", limit, expected)
25-
}
26-
}
27-
28-
func TestFromCgroupHybrid(t *testing.T) {
29-
if expected == 0 {
30-
t.Skip()
31-
}
32-
33-
limit, err := FromCgroupHybrid()
34-
if cgVersion == 0 && err != ErrNoCgroup {
35-
t.Fatalf("FromCgroupHybrid() error = %v, wantErr %v", err, ErrNoCgroup)
36-
}
37-
38-
if err != nil {
39-
t.Fatalf("FromCgroupHybrid() error = %v, wantErr %v", err, nil)
40-
}
41-
if limit != expected {
42-
t.Fatalf("FromCgroupHybrid() got = %v, want %v", limit, expected)
43-
}
44-
}
45-
46-
func TestFromCgroupV1(t *testing.T) {
47-
if expected == 0 || cgVersion != 1 {
48-
t.Skip()
49-
}
50-
limit, err := FromCgroupV1()
51-
if err != nil {
52-
t.Fatalf("FromCgroupV1() error = %v, wantErr %v", err, nil)
53-
}
54-
if limit != expected {
55-
t.Fatalf("FromCgroupV1() got = %v, want %v", limit, expected)
56-
}
57-
}
58-
59-
func TestFromCgroupV2(t *testing.T) {
60-
if expected == 0 || cgVersion != 2 {
61-
t.Skip()
62-
}
63-
limit, err := FromCgroupV2()
6423
if err != nil {
65-
t.Fatalf("FromCgroupV2() error = %v, wantErr %v", err, nil)
24+
t.Errorf("FromCgroup() error = %v, want nil", err)
6625
}
6726
if limit != expected {
68-
t.Fatalf("FromCgroupV2() got = %v, want %v", limit, expected)
27+
t.Errorf("FromCgroup() = %v, want %v", limit, expected)
6928
}
7029
}

0 commit comments

Comments
 (0)