From a9d3ee063cb8bc1f4d7d588c822249080662e24d Mon Sep 17 00:00:00 2001 From: "Ryan Keith (from Dev Box)" Date: Fri, 17 Jul 2026 13:23:21 -0700 Subject: [PATCH 1/2] GCS: periodically update cgroup limits Signed-off-by: Ryan Keith (from Dev Box) --- cmd/gcs/main.go | 100 +++++++++++++++++++++++++++++++++++++++-- cmd/gcs/main_test.go | 105 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 cmd/gcs/main_test.go diff --git a/cmd/gcs/main.go b/cmd/gcs/main.go index fd9575ec3b..64a35f16b5 100644 --- a/cmd/gcs/main.go +++ b/cmd/gcs/main.go @@ -113,6 +113,92 @@ func readMemoryEvents(startTime time.Time, efdFile *os.File, cgName string, thre } } +type cgroupUpdater interface { + Update(resources *oci.LinuxResources) error +} + +// Sanity checks the memory limit value +func calculateContainersMemoryLimit(totalMemoryBytes, rootMemReserveBytes uint64) (int64, error) { + if totalMemoryBytes <= rootMemReserveBytes { + return 0, errors.Errorf("total memory %d must be greater than root memory reserve %d", totalMemoryBytes, rootMemReserveBytes) + } + + limit := totalMemoryBytes - rootMemReserveBytes + const maxInt64 = uint64(1<<63 - 1) + if limit > maxInt64 { + return 0, errors.Errorf("containers cgroup memory limit %d exceeds maximum supported value %d", limit, maxInt64) + } + return int64(limit), nil +} + +// Returns the total system memory in bytes. +func currentTotalMemoryBytes() (uint64, error) { + sinfo := syscall.Sysinfo_t{} + if err := syscall.Sysinfo(&sinfo); err != nil { + return 0, errors.Wrap(err, "failed to get sys info") + } + + totalMemoryBytes := uint64(sinfo.Totalram) + unit := uint64(sinfo.Unit) + + // If the unit is non-zero, multiply the total memory by the unit to get the actual memory in bytes. + if unit != 0 { + const maxUint64 = ^uint64(0) + if totalMemoryBytes > maxUint64/unit { + return 0, errors.Errorf("total memory %d with unit %d exceeds maximum supported value", totalMemoryBytes, unit) + } + totalMemoryBytes *= unit + } + return totalMemoryBytes, nil +} + +// Set the cgroup memory limits for the containers and virtual-pods cgroups (pass-through to setCGroupMemoryLimits) +func updateCgroupMemoryLimits(podControl cgroupUpdater, rootMemReserveBytes uint64, lastAppliedLimit int64) (int64, bool, error) { + totalMemoryBytes, err := currentTotalMemoryBytes() + if err != nil { + return lastAppliedLimit, false, err + } + return setCgroupMemoryLimits(podControl, totalMemoryBytes, rootMemReserveBytes, lastAppliedLimit) +} + +// Set the cgroup memory limits for the containers and virtual-pods cgroups if they've changed +func setCgroupMemoryLimits(podControl cgroupUpdater, totalMemoryBytes, rootMemReserveBytes uint64, lastAppliedLimit int64) (int64, bool, error) { + podLimit, err := calculateContainersMemoryLimit(totalMemoryBytes, rootMemReserveBytes) + if err != nil { + return lastAppliedLimit, false, err + } + if podLimit == lastAppliedLimit { + return lastAppliedLimit, false, nil + } + resources := &oci.LinuxResources{ + Memory: &oci.LinuxMemory{Limit: &podLimit}, + } + + if err := podControl.Update(resources); err != nil { + return lastAppliedLimit, false, errors.Wrap(err, "failed to update containers cgroup memory limit") + } + return podLimit, true, nil +} + +// Start a periodic goroutine to monitor and update the cgroup memory limits for the containers and virtual-pods cgroups +func monitorCgroupMemoryLimits(containersControl cgroupUpdater, rootMemReserveBytes uint64, initialLimit int64, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + lastAppliedLimit := initialLimit + for range ticker.C { + podLimit, updated, err := updateCgroupMemoryLimits(containersControl, rootMemReserveBytes, lastAppliedLimit) + if err != nil { + logrus.WithError(err).Error("failed to refresh cgroup memory limits") + continue + } + if !updated { + continue + } + lastAppliedLimit = podLimit + logrus.WithField("memoryLimitBytes", podLimit).Debug("refreshed cgroup memory limits") + } +} + // runWithRestartMonitor starts a command with given args and waits for it to exit. If the // command exit code is non-zero the command is restarted with with some back off delay. // Any stdout or stderr of the command will be split into lines and written as a log with @@ -351,11 +437,14 @@ func main() { // // The gcs cgroup is not limited but an event will get logged if memory // usage exceeds 50 MB. - sinfo := syscall.Sysinfo_t{} - if err := syscall.Sysinfo(&sinfo); err != nil { - logrus.WithError(err).Fatal("failed to get sys info") + totalMemoryBytes, err := currentTotalMemoryBytes() + if err != nil { + logrus.WithError(err).Fatal("failed to get total system memory") + } + podsLimit, err := calculateContainersMemoryLimit(totalMemoryBytes, *rootMemReserveBytes) + if err != nil { + logrus.WithError(err).Fatal("failed to calculate pods cgroup memory limit") } - podsLimit := int64(sinfo.Totalram - *rootMemReserveBytes) podsControl, err := cgroup.NewManager("/pods", &oci.LinuxResources{ Memory: &oci.LinuxMemory{ Limit: &podsLimit, @@ -383,6 +472,9 @@ func main() { h := hcsv2.NewHost(rtime, tport, initialEnforcer, logWriter) + // Start periodic task to update cgroup memory limits for containers and virtual-pods cgroups + go monitorCgroupMemoryLimits(podsControl, *rootMemReserveBytes, podsLimit, 60*time.Second) + // During live migration the VM is frozen and only wakes up when the host // shim is ready, so the vsock port should be immediately available. We // use a tight retry interval instead of exponential backoff. diff --git a/cmd/gcs/main_test.go b/cmd/gcs/main_test.go new file mode 100644 index 0000000000..e29728fec7 --- /dev/null +++ b/cmd/gcs/main_test.go @@ -0,0 +1,105 @@ +//go:build linux +// +build linux + +package main + +import ( + "reflect" + "strings" + "testing" + + oci "github.com/opencontainers/runtime-spec/specs-go" +) + +func TestCalculateContainersMemoryLimit(t *testing.T) { + tests := []struct { + name string + total uint64 + reserve uint64 + want int64 + wantErrText string + }{ + {name: "subtracts reserve", total: 4096, reserve: 1024, want: 3072}, + {name: "rejects equal reserve", total: 1024, reserve: 1024, wantErrText: "must be greater"}, + {name: "rejects below reserve", total: 512, reserve: 1024, wantErrText: "must be greater"}, + {name: "rejects int64 overflow", total: uint64(1 << 63), wantErrText: "exceeds maximum"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := calculateContainersMemoryLimit(test.total, test.reserve) + if test.wantErrText != "" { + if err == nil || !strings.Contains(err.Error(), test.wantErrText) { + t.Fatalf("expected error containing %q, got %v", test.wantErrText, err) + } + return + } + if err != nil { + t.Fatalf("calculateContainersMemoryLimit returned error: %v", err) + } + if got != test.want { + t.Fatalf("calculateContainersMemoryLimit returned %d, want %d", got, test.want) + } + }) + } +} + +func TestSetCgroupMemoryLimitsUpdatesParentBeforeChild(t *testing.T) { + var updateOrder []string + podsControl := &testCgroupUpdater{name: "pods", updateOrder: &updateOrder} + + limit, updated, err := setCgroupMemoryLimits(podsControl, 4096, 1024, 2048) + if err != nil { + t.Fatalf("setCgroupMemoryLimits returned error: %v", err) + } + if !updated { + t.Fatal("setCgroupMemoryLimits did not report an update") + } + if limit != 3072 { + t.Fatalf("setCgroupMemoryLimits returned limit %d, want 3072", limit) + } + if want := []string{"pods"}; !reflect.DeepEqual(updateOrder, want) { + t.Fatalf("cgroups updated in order %v, want %v", updateOrder, want) + } + if podsControl.limit != 3072 { + t.Fatalf("cgroup limits are pods=%d, want 3072", podsControl.limit) + } +} + +func TestSetCgroupMemoryLimitsSkipsUnchangedLimit(t *testing.T) { + podsControl := &testCgroupUpdater{} + + limit, updated, err := setCgroupMemoryLimits(podsControl, 4096, 1024, 3072) + if err != nil { + t.Fatalf("setCgroupMemoryLimits returned error: %v", err) + } + if updated { + t.Fatal("setCgroupMemoryLimits reported an update for an unchanged limit") + } + if limit != 3072 { + t.Fatalf("setCgroupMemoryLimits returned limit %d, want 3072", limit) + } + if podsControl.updates != 0 { + t.Fatalf("unchanged limit updated cgroups: pods=%d", podsControl.updates) + } +} + +type testCgroupUpdater struct { + name string + limit int64 + updates int + updateErr error + updateOrder *[]string +} + +func (cgroup *testCgroupUpdater) Update(resources *oci.LinuxResources) error { + cgroup.updates++ + if cgroup.updateErr != nil { + return cgroup.updateErr + } + cgroup.limit = *resources.Memory.Limit + if cgroup.updateOrder != nil { + *cgroup.updateOrder = append(*cgroup.updateOrder, cgroup.name) + } + return nil +} From 093952ef49947df9f0cac30fe2ca108e70568c0a Mon Sep 17 00:00:00 2001 From: "Ryan Keith (from Dev Box)" Date: Tue, 21 Jul 2026 16:33:37 -0700 Subject: [PATCH 2/2] Modify mem cgroup limit during resize Signed-off-by: Ryan Keith (from Dev Box) --- cmd/gcs/main.go | 45 ++------ cmd/gcs/main_test.go | 63 ----------- internal/guest/prot/protocol.go | 2 + internal/guest/runtime/hcsv2/uvm.go | 101 +++++++++++++++++- .../guest/runtime/hcsv2/uvm_state_test.go | 88 +++++++++++++++ internal/protocol/guestresource/resources.go | 2 + internal/uvm/update_uvm.go | 10 ++ 7 files changed, 206 insertions(+), 105 deletions(-) diff --git a/cmd/gcs/main.go b/cmd/gcs/main.go index 64a35f16b5..e5eafdff6b 100644 --- a/cmd/gcs/main.go +++ b/cmd/gcs/main.go @@ -113,10 +113,6 @@ func readMemoryEvents(startTime time.Time, efdFile *os.File, cgName string, thre } } -type cgroupUpdater interface { - Update(resources *oci.LinuxResources) error -} - // Sanity checks the memory limit value func calculateContainersMemoryLimit(totalMemoryBytes, rootMemReserveBytes uint64) (int64, error) { if totalMemoryBytes <= rootMemReserveBytes { @@ -152,41 +148,12 @@ func currentTotalMemoryBytes() (uint64, error) { return totalMemoryBytes, nil } -// Set the cgroup memory limits for the containers and virtual-pods cgroups (pass-through to setCGroupMemoryLimits) -func updateCgroupMemoryLimits(podControl cgroupUpdater, rootMemReserveBytes uint64, lastAppliedLimit int64) (int64, bool, error) { - totalMemoryBytes, err := currentTotalMemoryBytes() - if err != nil { - return lastAppliedLimit, false, err - } - return setCgroupMemoryLimits(podControl, totalMemoryBytes, rootMemReserveBytes, lastAppliedLimit) -} - -// Set the cgroup memory limits for the containers and virtual-pods cgroups if they've changed -func setCgroupMemoryLimits(podControl cgroupUpdater, totalMemoryBytes, rootMemReserveBytes uint64, lastAppliedLimit int64) (int64, bool, error) { - podLimit, err := calculateContainersMemoryLimit(totalMemoryBytes, rootMemReserveBytes) - if err != nil { - return lastAppliedLimit, false, err - } - if podLimit == lastAppliedLimit { - return lastAppliedLimit, false, nil - } - resources := &oci.LinuxResources{ - Memory: &oci.LinuxMemory{Limit: &podLimit}, - } - - if err := podControl.Update(resources); err != nil { - return lastAppliedLimit, false, errors.Wrap(err, "failed to update containers cgroup memory limit") - } - return podLimit, true, nil -} - -// Start a periodic goroutine to monitor and update the cgroup memory limits for the containers and virtual-pods cgroups -func monitorCgroupMemoryLimits(containersControl cgroupUpdater, rootMemReserveBytes uint64, initialLimit int64, interval time.Duration) { +// Start a periodic goroutine to monitor and update the pods cgroup memory limit. +func monitorCgroupMemoryLimits(h *hcsv2.Host, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() - lastAppliedLimit := initialLimit for range ticker.C { - podLimit, updated, err := updateCgroupMemoryLimits(containersControl, rootMemReserveBytes, lastAppliedLimit) + podLimit, updated, err := h.UpdateCgroupMemoryLimits() if err != nil { logrus.WithError(err).Error("failed to refresh cgroup memory limits") continue @@ -194,7 +161,6 @@ func monitorCgroupMemoryLimits(containersControl cgroupUpdater, rootMemReserveBy if !updated { continue } - lastAppliedLimit = podLimit logrus.WithField("memoryLimitBytes", podLimit).Debug("refreshed cgroup memory limits") } } @@ -471,9 +437,10 @@ func main() { } h := hcsv2.NewHost(rtime, tport, initialEnforcer, logWriter) + h.InitializeCgroupMemoryLimitUpdater(podsControl, *rootMemReserveBytes, podsLimit) - // Start periodic task to update cgroup memory limits for containers and virtual-pods cgroups - go monitorCgroupMemoryLimits(podsControl, *rootMemReserveBytes, podsLimit, 60*time.Second) + // Start periodic task to update the pods cgroup memory limit. + go monitorCgroupMemoryLimits(h, 60*time.Second) // During live migration the VM is frozen and only wakes up when the host // shim is ready, so the vsock port should be immediately available. We diff --git a/cmd/gcs/main_test.go b/cmd/gcs/main_test.go index e29728fec7..46a74fc2e3 100644 --- a/cmd/gcs/main_test.go +++ b/cmd/gcs/main_test.go @@ -4,11 +4,8 @@ package main import ( - "reflect" "strings" "testing" - - oci "github.com/opencontainers/runtime-spec/specs-go" ) func TestCalculateContainersMemoryLimit(t *testing.T) { @@ -43,63 +40,3 @@ func TestCalculateContainersMemoryLimit(t *testing.T) { }) } } - -func TestSetCgroupMemoryLimitsUpdatesParentBeforeChild(t *testing.T) { - var updateOrder []string - podsControl := &testCgroupUpdater{name: "pods", updateOrder: &updateOrder} - - limit, updated, err := setCgroupMemoryLimits(podsControl, 4096, 1024, 2048) - if err != nil { - t.Fatalf("setCgroupMemoryLimits returned error: %v", err) - } - if !updated { - t.Fatal("setCgroupMemoryLimits did not report an update") - } - if limit != 3072 { - t.Fatalf("setCgroupMemoryLimits returned limit %d, want 3072", limit) - } - if want := []string{"pods"}; !reflect.DeepEqual(updateOrder, want) { - t.Fatalf("cgroups updated in order %v, want %v", updateOrder, want) - } - if podsControl.limit != 3072 { - t.Fatalf("cgroup limits are pods=%d, want 3072", podsControl.limit) - } -} - -func TestSetCgroupMemoryLimitsSkipsUnchangedLimit(t *testing.T) { - podsControl := &testCgroupUpdater{} - - limit, updated, err := setCgroupMemoryLimits(podsControl, 4096, 1024, 3072) - if err != nil { - t.Fatalf("setCgroupMemoryLimits returned error: %v", err) - } - if updated { - t.Fatal("setCgroupMemoryLimits reported an update for an unchanged limit") - } - if limit != 3072 { - t.Fatalf("setCgroupMemoryLimits returned limit %d, want 3072", limit) - } - if podsControl.updates != 0 { - t.Fatalf("unchanged limit updated cgroups: pods=%d", podsControl.updates) - } -} - -type testCgroupUpdater struct { - name string - limit int64 - updates int - updateErr error - updateOrder *[]string -} - -func (cgroup *testCgroupUpdater) Update(resources *oci.LinuxResources) error { - cgroup.updates++ - if cgroup.updateErr != nil { - return cgroup.updateErr - } - cgroup.limit = *resources.Memory.Limit - if cgroup.updateOrder != nil { - *cgroup.updateOrder = append(*cgroup.updateOrder, cgroup.name) - } - return nil -} diff --git a/internal/guest/prot/protocol.go b/internal/guest/prot/protocol.go index ef88bd278e..a09933d924 100644 --- a/internal/guest/prot/protocol.go +++ b/internal/guest/prot/protocol.go @@ -599,6 +599,8 @@ func UnmarshalContainerModifySettings(b []byte) (*containerModifySettings, error return &request, errors.Wrap(err, "failed to unmarshal settings as SecurityPolicyFragment") } msr.Settings = fragment + case guestresource.ResourceTypePodCgroupMemoryLimit: + msr.Settings = nil default: return &request, errors.Errorf("invalid ResourceType '%s'", msr.ResourceType) } diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index 45266e4193..49c0f352aa 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -95,9 +95,13 @@ type pod struct { // and processes. type Host struct { // containersMutex guards both containers and pods maps. - containersMutex sync.Mutex - containers map[string]*Container - pods map[string]*pod + containersMutex sync.Mutex + containers map[string]*Container + pods map[string]*pod + cgroupMemoryMutex sync.Mutex + podsCgroup cgroupUpdater + rootMemReserveBytes uint64 + lastAppliedCgroupMemLimit int64 externalProcessesMutex sync.Mutex externalProcesses map[int]*externalProcess @@ -131,6 +135,89 @@ type Host struct { uvmError uvmConsistencyError } +type cgroupUpdater interface { + Update(resources *specs.LinuxResources) error +} + +func calculateCgroupMemoryLimit(totalMemoryBytes, rootMemReserveBytes uint64) (int64, error) { + if totalMemoryBytes <= rootMemReserveBytes { + return 0, errors.Errorf("total memory %d must be greater than root memory reserve %d", totalMemoryBytes, rootMemReserveBytes) + } + + limit := totalMemoryBytes - rootMemReserveBytes + const maxInt64 = uint64(1<<63 - 1) + if limit > maxInt64 { + return 0, errors.Errorf("pods cgroup memory limit %d exceeds maximum supported value %d", limit, maxInt64) + } + return int64(limit), nil +} + +func currentTotalMemoryBytes() (uint64, error) { + sinfo := syscall.Sysinfo_t{} + if err := syscall.Sysinfo(&sinfo); err != nil { + return 0, errors.Wrap(err, "failed to get sys info") + } + + totalMemoryBytes := uint64(sinfo.Totalram) + unit := uint64(sinfo.Unit) + if unit != 0 { + const maxUint64 = ^uint64(0) + if totalMemoryBytes > maxUint64/unit { + return 0, errors.Errorf("total memory %d with unit %d exceeds maximum supported value", totalMemoryBytes, unit) + } + totalMemoryBytes *= unit + } + return totalMemoryBytes, nil +} + +func setCgroupMemoryLimit(podsCgroup cgroupUpdater, totalMemoryBytes, rootMemReserveBytes uint64, lastAppliedLimit int64) (int64, bool, error) { + limit, err := calculateCgroupMemoryLimit(totalMemoryBytes, rootMemReserveBytes) + if err != nil { + return lastAppliedLimit, false, err + } + if limit == lastAppliedLimit { + return limit, false, nil + } + + resources := &specs.LinuxResources{Memory: &specs.LinuxMemory{Limit: &limit}} + if err := podsCgroup.Update(resources); err != nil { + return lastAppliedLimit, false, errors.Wrap(err, "failed to update pods cgroup memory limit") + } + return limit, true, nil +} + +// InitializeCgroupMemoryLimitUpdater configures the cgroup updated when the UVM memory changes. +func (h *Host) InitializeCgroupMemoryLimitUpdater(podsCgroup cgroup.Manager, rootMemReserveBytes uint64, initialLimit int64) { + h.cgroupMemoryMutex.Lock() + defer h.cgroupMemoryMutex.Unlock() + + h.podsCgroup = podsCgroup + h.rootMemReserveBytes = rootMemReserveBytes + h.lastAppliedCgroupMemLimit = initialLimit +} + +// UpdateCgroupMemoryLimits refreshes the pods cgroup limit from the UVM's current memory size. +func (h *Host) UpdateCgroupMemoryLimits() (int64, bool, error) { + h.cgroupMemoryMutex.Lock() + defer h.cgroupMemoryMutex.Unlock() + + if h.podsCgroup == nil { + return h.lastAppliedCgroupMemLimit, false, errors.New("cgroup memory limit updater is not initialized") + } + totalMemoryBytes, err := currentTotalMemoryBytes() + if err != nil { + return h.lastAppliedCgroupMemLimit, false, err + } + limit, updated, err := setCgroupMemoryLimit(h.podsCgroup, totalMemoryBytes, h.rootMemReserveBytes, h.lastAppliedCgroupMemLimit) + if err != nil { + return h.lastAppliedCgroupMemLimit, false, err + } + if updated { + h.lastAppliedCgroupMemLimit = limit + } + return limit, updated, nil +} + type uvmConsistencyError struct { mu sync.Mutex // The error describing why the UVM has entered an inconsistent state. If @@ -925,6 +1012,14 @@ func (h *Host) modifyHostSettings(ctx context.Context, containerID string, req * return errors.New("the request settings are not of type SecurityPolicyFragment") } return h.securityOptions.InjectFragment(ctx, r) + case guestresource.ResourceTypePodCgroupMemoryLimit: + switch req.RequestType { + case guestrequest.RequestTypeUpdate: + _, _, err := h.UpdateCgroupMemoryLimits() + return err + default: + return newInvalidRequestTypeError(req.RequestType) + } default: return errors.Errorf("the ResourceType %q is not supported for UVM", req.ResourceType) } diff --git a/internal/guest/runtime/hcsv2/uvm_state_test.go b/internal/guest/runtime/hcsv2/uvm_state_test.go index 23dd1e50c3..f06db4c492 100644 --- a/internal/guest/runtime/hcsv2/uvm_state_test.go +++ b/internal/guest/runtime/hcsv2/uvm_state_test.go @@ -4,9 +4,97 @@ package hcsv2 import ( + "context" + "strings" "testing" + + "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" + "github.com/Microsoft/hcsshim/internal/protocol/guestresource" + oci "github.com/opencontainers/runtime-spec/specs-go" ) +func TestCalculateCgroupMemoryLimit(t *testing.T) { + tests := []struct { + name string + total uint64 + reserve uint64 + want int64 + wantErrText string + }{ + {name: "subtracts reserve", total: 4096, reserve: 1024, want: 3072}, + {name: "rejects equal reserve", total: 1024, reserve: 1024, wantErrText: "must be greater"}, + {name: "rejects below reserve", total: 512, reserve: 1024, wantErrText: "must be greater"}, + {name: "rejects int64 overflow", total: uint64(1 << 63), wantErrText: "exceeds maximum"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := calculateCgroupMemoryLimit(test.total, test.reserve) + if test.wantErrText != "" { + if err == nil || !strings.Contains(err.Error(), test.wantErrText) { + t.Fatalf("expected error containing %q, got %v", test.wantErrText, err) + } + return + } + if err != nil { + t.Fatalf("calculateCgroupMemoryLimit returned error: %v", err) + } + if got != test.want { + t.Fatalf("calculateCgroupMemoryLimit returned %d, want %d", got, test.want) + } + }) + } +} + +func TestSetCgroupMemoryLimit(t *testing.T) { + podsControl := &testCgroupUpdater{} + limit, updated, err := setCgroupMemoryLimit(podsControl, 4096, 1024, 2048) + if err != nil { + t.Fatalf("setCgroupMemoryLimit returned error: %v", err) + } + if !updated || limit != 3072 || podsControl.limit != 3072 { + t.Fatalf("setCgroupMemoryLimit returned limit=%d, updated=%t, applied=%d; want 3072, true, 3072", limit, updated, podsControl.limit) + } + + limit, updated, err = setCgroupMemoryLimit(podsControl, 4096, 1024, 3072) + if err != nil { + t.Fatalf("setCgroupMemoryLimit returned error: %v", err) + } + if updated || limit != 3072 || podsControl.updates != 1 { + t.Fatalf("unchanged set returned limit=%d, updated=%t, updates=%d; want 3072, false, 1", limit, updated, podsControl.updates) + } +} + +func TestModifyHostSettingsRejectsInvalidPodCgroupMemoryLimitRequestType(t *testing.T) { + for _, requestType := range []guestrequest.RequestType{ + guestrequest.RequestTypeAdd, + guestrequest.RequestTypeRemove, + guestrequest.RequestTypePreAdd, + } { + t.Run(string(requestType), func(t *testing.T) { + host := &Host{} + err := host.modifyHostSettings(context.Background(), UVMContainerID, &guestrequest.ModificationRequest{ + ResourceType: guestresource.ResourceTypePodCgroupMemoryLimit, + RequestType: requestType, + }) + if err == nil || !strings.Contains(err.Error(), "RequestType") { + t.Fatalf("modifyHostSettings returned %v, want invalid RequestType error", err) + } + }) + } +} + +type testCgroupUpdater struct { + limit int64 + updates int +} + +func (cgroup *testCgroupUpdater) Update(resources *oci.LinuxResources) error { + cgroup.updates++ + cgroup.limit = *resources.Memory.Limit + return nil +} + func Test_Add_Remove_RWDevice(t *testing.T) { hm := newHostMounts() mountPath := "/run/gcs/c/abcd" diff --git a/internal/protocol/guestresource/resources.go b/internal/protocol/guestresource/resources.go index c7eecfe140..932b97290f 100644 --- a/internal/protocol/guestresource/resources.go +++ b/internal/protocol/guestresource/resources.go @@ -55,6 +55,8 @@ const ( ResourceTypeSecurityPolicy guestrequest.ResourceType = "SecurityPolicy" // ResourceTypePolicyFragment is the modify resource type for injecting policy fragments. ResourceTypePolicyFragment guestrequest.ResourceType = "SecurityPolicyFragment" + // ResourceTypePodCgroupMemoryLimit is used to force a cgroup memory limit update in the guest. + ResourceTypePodCgroupMemoryLimit guestrequest.ResourceType = "PodCgroupMemoryLimit" ) // This class is used by a modify request to add or remove a combined layers diff --git a/internal/uvm/update_uvm.go b/internal/uvm/update_uvm.go index 86b988e090..129d690010 100644 --- a/internal/uvm/update_uvm.go +++ b/internal/uvm/update_uvm.go @@ -7,6 +7,8 @@ import ( "fmt" hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" + "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" + "github.com/Microsoft/hcsshim/internal/protocol/guestresource" "github.com/Microsoft/hcsshim/pkg/annotations" "github.com/Microsoft/hcsshim/pkg/ctrdtaskapi" "github.com/opencontainers/runtime-spec/specs-go" @@ -54,6 +56,14 @@ func (uvm *UtilityVM) Update(ctx context.Context, data interface{}, annots map[s if err := uvm.UpdateMemory(ctx, *memoryLimitInBytes); err != nil { return err } + if uvm.operatingSystem == "linux" { + if err := uvm.GuestRequest(ctx, guestrequest.ModificationRequest{ + ResourceType: guestresource.ResourceTypePodCgroupMemoryLimit, + RequestType: guestrequest.RequestTypeUpdate, + }); err != nil { + return err + } + } } if processorLimits != nil { if err := uvm.UpdateCPULimits(ctx, processorLimits); err != nil {