// Copyright (c) 2022 Gitpod GmbH. All rights reserved. // Licensed under the GNU Affero General Public License (AGPL). // See License-AGPL.txt in the project root for license information. package cgroup import ( "context" "path/filepath" "strconv" "strings" "sync" v2 "github.com/containerd/cgroups/v2" "github.com/gitpod-io/gitpod/common-go/log" ) type IOLimiterV2 struct { limits *v2.Resources cond *sync.Cond devices []string } func NewIOLimiterV2(writeBytesPerSecond, readBytesPerSecond, writeIOPs, readIOPs int64) (*IOLimiterV2, error) { devices := buildDevices() log.WithField("devices", devices).Debug("io limiting devices") return &IOLimiterV2{ limits: buildV2Limits(writeBytesPerSecond, readBytesPerSecond, writeIOPs, readIOPs, devices), cond: sync.NewCond(&sync.Mutex{}), devices: devices, }, nil } func (c *IOLimiterV2) Name() string { return "iolimiter-v2" } func (c *IOLimiterV2) Type() Version { return Version2 } func (c *IOLimiterV2) Apply(ctx context.Context, opts *PluginOptions) error { update := make(chan struct{}, 1) go func() { // TODO(cw): this Go-routine will leak per workspace, until we update config or restart ws-daemon defer close(update) for { c.cond.L.Lock() c.cond.Wait() c.cond.L.Unlock() if ctx.Err() != nil { return } update <- struct{}{} } }() go func() { log.WithField("cgroupPath", opts.CgroupPath).Debug("starting io limiting") _, err := v2.NewManager(opts.BasePath, filepath.Join("/", opts.CgroupPath), c.limits) if err != nil { log.WithError(err).WithField("basePath", opts.BasePath).WithField("cgroupPath", opts.CgroupPath).WithField("limits", c.limits).Error("cannot write IO limits") } for { select { case <-update: _, err := v2.NewManager(opts.BasePath, filepath.Join("/", opts.CgroupPath), c.limits) if err != nil { log.WithError(err).WithField("basePath", opts.BasePath).WithField("cgroupPath", opts.CgroupPath).WithField("limits", c.limits).Error("cannot write IO limits") } case <-ctx.Done(): // Prior to shutting down though, we need to reset the IO limits to ensure we don't have // processes stuck in the uninterruptable "D" (disk sleep) state. This would prevent the // workspace pod from shutting down. _, err := v2.NewManager(opts.BasePath, filepath.Join("/", opts.CgroupPath), &v2.Resources{}) if err != nil { log.WithError(err).WithField("cgroupPath", opts.CgroupPath).Error("cannot write IO limits") } log.WithField("cgroupPath", opts.CgroupPath).Debug("stopping io limiting") return } } }() return nil } func (c *IOLimiterV2) Update(writeBytesPerSecond, readBytesPerSecond, writeIOPs, readIOPs int64) { c.cond.L.Lock() defer c.cond.L.Unlock() c.limits = buildV2Limits(writeBytesPerSecond, readBytesPerSecond, writeIOPs, readIOPs, c.devices) log.WithField("limits", c.limits.IO).Info("updating I/O cgroups v2 limits") c.cond.Broadcast() } func buildV2Limits(writeBytesPerSecond, readBytesPerSecond, writeIOPs, readIOPs int64, devices []string) *v2.Resources { resources := &v2.Resources{ IO: &v2.IO{}, } for _, device := range devices { majmin := strings.Split(device, ":") if len(majmin) != 2 { log.WithField("device", device).Error("invalid device") continue } major, err := strconv.ParseInt(majmin[0], 10, 64) if err != nil { log.WithError(err).Error("invalid major device") continue } minor, err := strconv.ParseInt(majmin[1], 10, 64) if err != nil { log.WithError(err).Error("invalid minor device") continue } if readBytesPerSecond > 0 { resources.IO.Max = append(resources.IO.Max, v2.Entry{Major: major, Minor: minor, Type: v2.ReadBPS, Rate: uint64(readBytesPerSecond)}) } if readIOPs > 0 { resources.IO.Max = append(resources.IO.Max, v2.Entry{Major: major, Minor: minor, Type: v2.ReadIOPS, Rate: uint64(readIOPs)}) } if writeBytesPerSecond > 0 { resources.IO.Max = append(resources.IO.Max, v2.Entry{Major: major, Minor: minor, Type: v2.WriteBPS, Rate: uint64(writeBytesPerSecond)}) } if writeIOPs > 0 { resources.IO.Max = append(resources.IO.Max, v2.Entry{Major: major, Minor: minor, Type: v2.WriteIOPS, Rate: uint64(writeIOPs)}) } } log.WithField("resources", resources).Debug("cgroups v2 limits") return resources }