-
Notifications
You must be signed in to change notification settings - Fork 2
Add NetworkLimiter with bidirectional bandwidth limiting via tc integration #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 6 commits
ce29fee
5568305
c0dbecf
6d71b93
c8e4952
5ed595e
de4fa83
595e495
0584d41
0af5d28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,34 @@ | ||
| package executor | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
|
|
||
| "github.com/pmarchini/giogo/internal/core" | ||
| "github.com/pmarchini/giogo/internal/limiter" | ||
|
|
||
| specs "github.com/opencontainers/runtime-spec/specs-go" | ||
| ) | ||
|
|
||
| type Executor struct { | ||
| Limiters []limiter.ResourceLimiter | ||
| Limiters []limiter.ResourceLimiter | ||
| NetworkLimiter *limiter.NetworkLimiter | ||
| } | ||
|
|
||
| func NewExecutor(limiters []limiter.ResourceLimiter) *Executor { | ||
| return &Executor{ | ||
| executor := &Executor{ | ||
| Limiters: limiters, | ||
| } | ||
|
|
||
| // Extract NetworkLimiter if present for special handling | ||
| for _, l := range limiters { | ||
| if netLimiter, ok := l.(*limiter.NetworkLimiter); ok { | ||
| executor.NetworkLimiter = netLimiter | ||
| break | ||
| } | ||
| } | ||
|
|
||
| return executor | ||
| } | ||
|
|
||
| func (e *Executor) RunCommand(args []string) error { | ||
|
|
@@ -23,6 +37,25 @@ func (e *Executor) RunCommand(args []string) error { | |
| l.Apply(&resources) | ||
| } | ||
|
|
||
| // Set up traffic control if network limiter with bandwidth is configured | ||
| if e.NetworkLimiter != nil && e.NetworkLimiter.MaxBandwidth > 0 { | ||
|
||
| // Get default interface - in production this could be configurable | ||
| iface := limiter.GetDefaultInterface() | ||
|
|
||
| // Setup tc before running the command | ||
| if err := e.NetworkLimiter.SetupTrafficControl(iface); err != nil { | ||
| return fmt.Errorf("failed to setup traffic control: %v", err) | ||
| } | ||
|
|
||
| // Ensure cleanup happens when we're done | ||
| defer func() { | ||
| if err := e.NetworkLimiter.CleanupTrafficControl(iface); err != nil { | ||
| // Non-fatal: log the error but don't fail the command | ||
| fmt.Fprintf(os.Stderr, "Warning: failed to cleanup traffic control: %v\n", err) | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| coreModule, err := core.NewCore(resources) | ||
| if err != nil { | ||
| return err | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| package limiter | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| specs "github.com/opencontainers/runtime-spec/specs-go" | ||
| ) | ||
|
|
||
| // Base error for NetworkLimiter | ||
| var ErrInvalidNetworkValue = errors.New("invalid network limiter value") | ||
|
|
||
| // NetworkLimiterError represents a custom error with a specific message and underlying cause | ||
| type NetworkLimiterError struct { | ||
| Message string | ||
| Cause error | ||
| } | ||
|
|
||
| func (e *NetworkLimiterError) Error() string { | ||
| return fmt.Sprintf("%s: %v", e.Message, e.Cause) | ||
| } | ||
|
|
||
| func (e *NetworkLimiterError) Is(target error) bool { | ||
| return errors.Is(e.Cause, target) | ||
| } | ||
|
|
||
| // Custom errors | ||
| var ( | ||
| ErrUnparsableClassID = &NetworkLimiterError{Message: "unparsable class ID value", Cause: ErrInvalidNetworkValue} | ||
| ErrUnparsablePriority = &NetworkLimiterError{Message: "unparsable priority value", Cause: ErrInvalidNetworkValue} | ||
| ErrUnparsableBandwidth = &NetworkLimiterError{Message: "unparsable bandwidth value", Cause: ErrInvalidNetworkValue} | ||
| ) | ||
|
|
||
| // NetworkLimiter applies network resource limits | ||
| type NetworkLimiter struct { | ||
| ClassID *uint32 | ||
| Priority *uint32 | ||
| MaxBandwidth uint64 // Maximum bandwidth in bytes per second (0 means unlimited) | ||
| } | ||
|
|
||
| // Apply the network limits to the provided Linux resources | ||
| func (n *NetworkLimiter) Apply(resources *specs.LinuxResources) { | ||
| if resources.Network == nil { | ||
| resources.Network = &specs.LinuxNetwork{} | ||
| } | ||
|
|
||
| if n.ClassID != nil { | ||
| resources.Network.ClassID = n.ClassID | ||
| } | ||
|
|
||
| if n.Priority != nil { | ||
| // Note: The spec uses LinuxInterfacePriority which requires an interface name | ||
| // We use an empty string to apply the priority to all interfaces | ||
| // This can be extended in the future to support per-interface priorities | ||
| resources.Network.Priorities = []specs.LinuxInterfacePriority{ | ||
| { | ||
| Name: "", // Empty string applies to all interfaces | ||
| Priority: *n.Priority, | ||
| }, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // NetworkLimiterInitializer holds the initialization parameters for NetworkLimiter | ||
| type NetworkLimiterInitializer struct { | ||
| ClassID string | ||
| Priority string | ||
| MaxBandwidth string | ||
| } | ||
|
|
||
| // NewNetworkLimiter creates a new NetworkLimiter with validation and error handling | ||
| func NewNetworkLimiter(init *NetworkLimiterInitializer) (*NetworkLimiter, error) { | ||
| limiter := &NetworkLimiter{} | ||
|
|
||
| if init.ClassID != "" { | ||
| classID, err := strconv.ParseUint(init.ClassID, 10, 32) | ||
| if err != nil { | ||
| return nil, ErrUnparsableClassID | ||
| } | ||
| classIDValue := uint32(classID) | ||
| limiter.ClassID = &classIDValue | ||
| } | ||
|
|
||
| if init.Priority != "" { | ||
| priority, err := strconv.ParseUint(init.Priority, 10, 32) | ||
| if err != nil { | ||
| return nil, ErrUnparsablePriority | ||
| } | ||
| priorityValue := uint32(priority) | ||
| limiter.Priority = &priorityValue | ||
| } | ||
|
|
||
| if init.MaxBandwidth != "" { | ||
| bandwidth, err := parseBandwidth(init.MaxBandwidth) | ||
| if err != nil { | ||
| return nil, &NetworkLimiterError{Message: "unparsable bandwidth value", Cause: err} | ||
| } | ||
| limiter.MaxBandwidth = bandwidth | ||
| } | ||
|
|
||
| return limiter, nil | ||
| } | ||
|
|
||
| // parseBandwidth parses bandwidth string (e.g., "1m", "500k") to bytes per second | ||
| func parseBandwidth(s string) (uint64, error) { | ||
| s = strings.TrimSpace(s) | ||
| var multiplier int64 = 1 | ||
| if strings.HasSuffix(s, "g") || strings.HasSuffix(s, "G") { | ||
| multiplier = 1024 * 1024 * 1024 | ||
| s = s[:len(s)-1] | ||
| } else if strings.HasSuffix(s, "m") || strings.HasSuffix(s, "M") { | ||
| multiplier = 1024 * 1024 | ||
| s = s[:len(s)-1] | ||
| } else if strings.HasSuffix(s, "k") || strings.HasSuffix(s, "K") { | ||
| multiplier = 1024 | ||
| s = s[:len(s)-1] | ||
| } else { | ||
| multiplier = 1 | ||
| } | ||
| value, err := strconv.ParseFloat(s, 64) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| return uint64(value * float64(multiplier)), nil | ||
| } | ||
|
|
||
| // SetupTrafficControl sets up tc (traffic control) rules for bandwidth limiting | ||
| // This method should be called after the cgroup is created and classID is set | ||
| func (n *NetworkLimiter) SetupTrafficControl(interfaceName string) error { | ||
| // Only setup tc if we have both classID and bandwidth limit | ||
| if n.ClassID == nil || n.MaxBandwidth == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| return setupHTB(interfaceName, *n.ClassID, n.MaxBandwidth) | ||
| } | ||
|
|
||
| // CleanupTrafficControl removes tc rules set up by SetupTrafficControl | ||
| func (n *NetworkLimiter) CleanupTrafficControl(interfaceName string) error { | ||
| // Only cleanup if we have a classID (indicating we set up tc) | ||
| if n.ClassID == nil || n.MaxBandwidth == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| return cleanupHTB(interfaceName) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not break the design having special handling here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 595e495. Refactored to use
LifecycleLimiterinterface instead of special handling:LifecycleLimiterinterface with Setup/Cleanup methodsThe design now maintains clean separation of concerns with interface-based patterns.