Skip to content

Commit 0432ddd

Browse files
committed
Add startup system limit tuning
1 parent 272dfa4 commit 0432ddd

8 files changed

Lines changed: 198 additions & 0 deletions

chicha-ip-proxy.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/matveynator/chicha-ip-proxy/pkg/config"
13+
"github.com/matveynator/chicha-ip-proxy/pkg/limits"
1314
"github.com/matveynator/chicha-ip-proxy/pkg/logging"
1415
"github.com/matveynator/chicha-ip-proxy/pkg/proxy"
1516
"github.com/matveynator/chicha-ip-proxy/pkg/setup"
@@ -88,6 +89,10 @@ func main() {
8889
log.Fatalf("Error setting up logger: %v", err)
8990
}
9091

92+
if err := limits.SetupLimits(logger); err != nil {
93+
logger.Printf("System limit tuning encountered an issue: %v", err)
94+
}
95+
9196
log.Printf("Starting chicha-ip-proxy version %s", version)
9297

9398
numCPUs := runtime.NumCPU()

pkg/limits/limits.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Package limits manages OS-specific resource limits to keep the proxy responsive under heavy load.
2+
// The code favors channels over mutexes so limit adjustments remain observable and coordinated.
3+
package limits
4+
5+
import (
6+
"fmt"
7+
"log"
8+
"time"
9+
)
10+
11+
type limitRequest struct {
12+
description string
13+
apply func() error
14+
}
15+
16+
// SetupLimits applies platform-specific limit changes in a channel-driven pipeline.
17+
// Using goroutines ensures each adjustment can proceed without blocking unrelated work.
18+
func SetupLimits(logger *log.Logger) error {
19+
requests := collectLimitRequests(logger)
20+
if len(requests) == 0 {
21+
logger.Printf("No system limit changes required on this platform")
22+
return nil
23+
}
24+
25+
requestChan := make(chan limitRequest)
26+
resultChan := make(chan error)
27+
28+
go func() {
29+
defer close(resultChan)
30+
for req := range requestChan {
31+
logger.Printf("Applying system limit: %s", req.description)
32+
resultChan <- req.apply()
33+
}
34+
}()
35+
36+
go func() {
37+
defer close(requestChan)
38+
for _, req := range requests {
39+
requestChan <- req
40+
}
41+
}()
42+
43+
for processed := 0; processed < len(requests); processed++ {
44+
select {
45+
case err := <-resultChan:
46+
if err != nil {
47+
return fmt.Errorf("system limit adjustment failed: %w", err)
48+
}
49+
case <-time.After(5 * time.Second):
50+
return fmt.Errorf("timeout while applying system limits")
51+
}
52+
}
53+
54+
return nil
55+
}

pkg/limits/limits_posix.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//go:build linux || darwin || freebsd || openbsd
2+
3+
// Package limits includes POSIX-specific limit tuning to mirror xinetd-like defaults.
4+
// Separating platforms keeps platform quirks localized and easier to maintain.
5+
package limits
6+
7+
import (
8+
"fmt"
9+
"log"
10+
"syscall"
11+
)
12+
13+
func collectLimitRequests(logger *log.Logger) []limitRequest {
14+
desiredOpenFiles := uint64(100000)
15+
desiredProcesses := uint64(100000)
16+
17+
requests := []limitRequest{
18+
buildInfinityRequest("virtual memory (rlimit_as)", syscall.RLIMIT_AS),
19+
buildInfinityRequest("CPU time (rlimit_cpu)", syscall.RLIMIT_CPU),
20+
buildTargetRequest("open files (rlimit_files)", syscall.RLIMIT_NOFILE, desiredOpenFiles, logger),
21+
}
22+
23+
if procResource, ok := processLimitResource(); ok {
24+
requests = append(requests, buildTargetRequest("process count (rlimit_proc)", procResource, desiredProcesses, logger))
25+
} else {
26+
logger.Printf("Process limit resource is unavailable on this platform; skipping rlimit_proc")
27+
}
28+
29+
return requests
30+
}
31+
32+
func buildInfinityRequest(label string, resource int) limitRequest {
33+
return limitRequest{
34+
description: fmt.Sprintf("%s -> unlimited", label),
35+
apply: func() error {
36+
unlimited := ^uint64(0)
37+
38+
current := &syscall.Rlimit{}
39+
if err := syscall.Getrlimit(resource, current); err != nil {
40+
return fmt.Errorf("failed reading %s: %w", label, err)
41+
}
42+
43+
desired := &syscall.Rlimit{Cur: unlimited, Max: unlimited}
44+
if current.Cur == desired.Cur && current.Max == desired.Max {
45+
return nil
46+
}
47+
48+
if err := syscall.Setrlimit(resource, desired); err != nil {
49+
return fmt.Errorf("failed setting %s to unlimited: %w", label, err)
50+
}
51+
return nil
52+
},
53+
}
54+
}
55+
56+
func buildTargetRequest(label string, resource int, target uint64, logger *log.Logger) limitRequest {
57+
return limitRequest{
58+
description: fmt.Sprintf("%s -> %d", label, target),
59+
apply: func() error {
60+
current := &syscall.Rlimit{}
61+
if err := syscall.Getrlimit(resource, current); err != nil {
62+
return fmt.Errorf("failed reading %s: %w", label, err)
63+
}
64+
65+
desired := &syscall.Rlimit{Cur: target, Max: target}
66+
if current.Max > desired.Max {
67+
desired.Max = current.Max
68+
}
69+
if desired.Cur > desired.Max {
70+
desired.Cur = desired.Max
71+
}
72+
73+
if current.Cur >= desired.Cur && current.Max >= desired.Max {
74+
return nil
75+
}
76+
77+
if err := syscall.Setrlimit(resource, desired); err != nil {
78+
logger.Printf("Adjusting %s hit %v; trying best-effort with existing max", label, err)
79+
fallback := &syscall.Rlimit{Cur: desired.Cur, Max: current.Max}
80+
if fallback.Cur > fallback.Max {
81+
fallback.Cur = fallback.Max
82+
}
83+
if setErr := syscall.Setrlimit(resource, fallback); setErr != nil {
84+
return fmt.Errorf("failed setting %s even after fallback: %w", label, setErr)
85+
}
86+
}
87+
return nil
88+
},
89+
}
90+
}

pkg/limits/limits_proc_darwin.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build darwin
2+
3+
// Package limits records the Darwin RLIMIT_NPROC identifier without pulling extra headers.
4+
// Explicit constants keep cross-compilation predictable for release automation.
5+
package limits
6+
7+
func processLimitResource() (int, bool) {
8+
return 7, true
9+
}

pkg/limits/limits_proc_freebsd.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build freebsd
2+
3+
// Package limits defines FreeBSD's RLIMIT_NPROC numeric identifier for process cap tuning.
4+
// Encoding it here keeps the rest of the codebase decoupled from cgo or external packages.
5+
package limits
6+
7+
func processLimitResource() (int, bool) {
8+
return 8, true
9+
}

pkg/limits/limits_proc_linux.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build linux
2+
3+
// Package limits embeds Linux-specific resource numbers to avoid external dependencies.
4+
// Keeping the constant here prevents cross-platform files from importing non-standard modules.
5+
package limits
6+
7+
func processLimitResource() (int, bool) {
8+
return 6, true
9+
}

pkg/limits/limits_proc_openbsd.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build openbsd
2+
3+
// Package limits includes the OpenBSD RLIMIT_NPROC value so process ceilings can be raised at startup.
4+
// Hardcoding the numeric value keeps the build pure Go without extra dependencies.
5+
package limits
6+
7+
func processLimitResource() (int, bool) {
8+
return 7, true
9+
}

pkg/limits/limits_windows.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//go:build windows
2+
3+
// Package limits documents the lack of configurable RLIMIT-style knobs on Windows.
4+
// The stub still runs through the channel pipeline so the caller sees consistent behavior.
5+
package limits
6+
7+
import "log"
8+
9+
func collectLimitRequests(logger *log.Logger) []limitRequest {
10+
logger.Printf("Windows relies on dynamic kernel limits; no explicit RLIMIT tuning applied")
11+
return nil
12+
}

0 commit comments

Comments
 (0)