Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions cmd/containerd-shim-runhcs-v1/task_hcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,21 @@ func (ht *hcsTask) updateWCOWContainerCPU(ctx context.Context, cpu *specs.Window
if cpu.Shares != nil {
req.Weight = int32(*cpu.Shares)
}
if len(cpu.Affinity) > 0 {
// Create a temporary spec to reuse the existing ConvertCPUAffinity validation
tempSpec := &specs.Spec{
Windows: &specs.Windows{
Resources: &specs.WindowsResources{
CPU: cpu,
},
},
}
affinity, err := hcsoci.ConvertCPUAffinity(tempSpec)
if err != nil {
return err
}
req.Affinity = affinity
}
return ht.requestUpdateContainer(ctx, resourcepaths.SiloProcessorResourcePath, req)
}

Expand Down
2 changes: 2 additions & 0 deletions internal/hcs/schema2/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ type Processor struct {
Maximum int32 `json:"Maximum,omitempty"`

Weight int32 `json:"Weight,omitempty"`

Affinity uint64 `json:"Affinity,omitempty"`
}
37 changes: 34 additions & 3 deletions internal/hcsoci/hcsdoc_wcow.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,31 @@ func createMountsConfig(ctx context.Context, coi *createOptionsInternal) (*mount
return &config, nil
}

// ConvertCPUAffinity handles the logic of converting and validating the container's CPU affinity
// specified in the OCI spec to what HCS expects.
//
// Returns the CPU affinity bitmask (0 if not specified) and any validation error.
// Phase 2 limitations:
// - Multiple affinity entries are rejected
// - Non-zero processor groups are rejected
func ConvertCPUAffinity(spec *specs.Spec) (uint64, error) {
if spec.Windows == nil || spec.Windows.Resources == nil || spec.Windows.Resources.CPU == nil || len(spec.Windows.Resources.CPU.Affinity) == 0 {
return 0, nil
}

affinity := spec.Windows.Resources.CPU.Affinity
if len(affinity) != 1 {
return 0, fmt.Errorf("cpu affinity with multiple processor groups is not supported")
}
if affinity[0].Group != 0 {
return 0, fmt.Errorf("cpu affinity processor group %d is not supported", affinity[0].Group)
}
if affinity[0].Mask == 0 {
return 0, fmt.Errorf("cpu affinity mask must be non-zero")
}
return affinity[0].Mask, nil
}

// ConvertCPULimits handles the logic of converting and validating the containers CPU limits
// specified in the OCI spec to what HCS expects.
//
Expand Down Expand Up @@ -184,6 +209,11 @@ func createWindowsContainerDocument(ctx context.Context, coi *createOptionsInter
return nil, nil, err
}

cpuAffinity, err := ConvertCPUAffinity(coi.Spec)
if err != nil {
return nil, nil, err
}

if coi.HostingSystem != nil && coi.ScaleCPULimitsToSandbox && cpuLimit > 0 {
// When ScaleCPULimitsToSandbox is set and we are running in a UVM, we assume
// the CPU limit has been calculated based on the number of processors on the
Expand Down Expand Up @@ -233,9 +263,10 @@ func createWindowsContainerDocument(ctx context.Context, coi *createOptionsInter
v1.ProcessorWeight = uint64(cpuWeight)

v2Container.Processor = &hcsschema.Processor{
Count: cpuCount,
Maximum: cpuLimit,
Weight: cpuWeight,
Count: cpuCount,
Maximum: cpuLimit,
Weight: cpuWeight,
Affinity: cpuAffinity,
}

// Memory Resources
Expand Down
149 changes: 149 additions & 0 deletions internal/hcsoci/hcsdoc_wcow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//go:build windows

package hcsoci

import (
"strings"
"testing"

specs "github.com/opencontainers/runtime-spec/specs-go"
)

func TestConvertCPUAffinity_Group0MaskSet(t *testing.T) {
s := &specs.Spec{
Windows: &specs.Windows{
Resources: &specs.WindowsResources{
CPU: &specs.WindowsCPUResources{
Affinity: []specs.WindowsCPUGroupAffinity{
{Mask: 0x3, Group: 0},
},
},
},
},
}

affinity, err := ConvertCPUAffinity(s)
if err != nil {
t.Fatalf("ConvertCPUAffinity failed: %v", err)
}
if affinity != 0x3 {
t.Fatalf("unexpected cpu affinity: got %d want %d", affinity, uint64(0x3))
}
}

func TestConvertCPUAffinity_MultiGroupRejected(t *testing.T) {
s := &specs.Spec{
Windows: &specs.Windows{
Resources: &specs.WindowsResources{
CPU: &specs.WindowsCPUResources{
Affinity: []specs.WindowsCPUGroupAffinity{
{Mask: 0x1, Group: 0},
{Mask: 0x1, Group: 1},
},
},
},
},
}

_, err := ConvertCPUAffinity(s)
if err == nil {
t.Fatal("expected error for multiple affinity entries")
}
if !strings.Contains(err.Error(), "multiple processor groups") {
t.Fatalf("unexpected error: %v", err)
}
}

func TestConvertCPUAffinity_NonZeroGroupRejected(t *testing.T) {
s := &specs.Spec{
Windows: &specs.Windows{
Resources: &specs.WindowsResources{
CPU: &specs.WindowsCPUResources{
Affinity: []specs.WindowsCPUGroupAffinity{
{Mask: 0x1, Group: 1},
},
},
},
},
}

_, err := ConvertCPUAffinity(s)
if err == nil {
t.Fatal("expected error for non-zero affinity group")
}
if !strings.Contains(err.Error(), "processor group") {
t.Fatalf("unexpected error: %v", err)
}
}

func TestConvertCPUAffinity_ZeroMaskRejected(t *testing.T) {
s := &specs.Spec{
Windows: &specs.Windows{
Resources: &specs.WindowsResources{
CPU: &specs.WindowsCPUResources{
Affinity: []specs.WindowsCPUGroupAffinity{
{Mask: 0, Group: 0},
},
},
},
},
}

_, err := ConvertCPUAffinity(s)
if err == nil {
t.Fatal("expected error for zero affinity mask")
}
if !strings.Contains(err.Error(), "mask must be non-zero") {
t.Fatalf("unexpected error: %v", err)
}
}

func TestConvertCPUAffinity_NoAffinity(t *testing.T) {
testCases := []struct {
name string
spec *specs.Spec
}{
{
name: "nil spec.Windows",
spec: &specs.Spec{},
},
{
name: "nil spec.Windows.Resources",
spec: &specs.Spec{
Windows: &specs.Windows{},
},
},
{
name: "nil spec.Windows.Resources.CPU",
spec: &specs.Spec{
Windows: &specs.Windows{
Resources: &specs.WindowsResources{},
},
},
},
{
name: "empty affinity slice",
spec: &specs.Spec{
Windows: &specs.Windows{
Resources: &specs.WindowsResources{
CPU: &specs.WindowsCPUResources{
Affinity: []specs.WindowsCPUGroupAffinity{},
},
},
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
affinity, err := ConvertCPUAffinity(tc.spec)
if err != nil {
t.Fatalf("ConvertCPUAffinity failed: %v", err)
}
if affinity != 0 {
t.Fatalf("expected zero affinity, got %d", affinity)
}
})
}
}
Loading