Skip to content

Commit 4782c00

Browse files
author
Weyang1
committed
hcsoci: address PR review comments
- Remove Affinity uint64 from schema2/Processor (not a real HCS API field); preserve ConvertCPUAffinity validation with a TODO to wire it properly once the correct schema field is confirmed - Define sentinel errors (ErrCPUAffinityMultipleGroups, ErrCPUAffinityNonZeroGroup, ErrCPUAffinityMaskZero) and replace strings.Contains checks in tests with errors.Is for more robust error identity verification Signed-off-by: Weyang1 <weyang1@microsoft.com>
1 parent 95fdd2d commit 4782c00

10 files changed

Lines changed: 321 additions & 80 deletions

File tree

cmd/containerd-shim-runhcs-v1/task_hcs.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -935,19 +935,26 @@ func (ht *hcsTask) updateWCOWContainerCPU(ctx context.Context, cpu *specs.Window
935935
req.Weight = int32(*cpu.Shares)
936936
}
937937
if len(cpu.Affinity) > 0 {
938-
// Create a temporary spec to reuse the existing ConvertCPUAffinity validation
938+
// Validate and retrieve CPU affinity.
939939
tempSpec := &specs.Spec{
940940
Windows: &specs.Windows{
941941
Resources: &specs.WindowsResources{
942942
CPU: cpu,
943943
},
944944
},
945945
}
946-
affinity, err := hcsoci.ConvertCPUAffinity(tempSpec)
946+
affinities, err := hcsoci.ConvertCPUAffinity(tempSpec)
947947
if err != nil {
948948
return err
949949
}
950-
req.Affinity = affinity
950+
groupAffs := make([]hcsschema.ProcessorGroupAffinity, len(affinities))
951+
for i, a := range affinities {
952+
groupAffs[i] = hcsschema.ProcessorGroupAffinity{
953+
Mask: a.Mask,
954+
Group: uint16(a.Group),
955+
}
956+
}
957+
req.GroupAffinities = groupAffs
951958
}
952959
return ht.requestUpdateContainer(ctx, resourcepaths.SiloProcessorResourcePath, req)
953960
}

internal/hcs/schema2/processor.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
55
*
6-
* API version: 2.1
6+
* API version: 2.4
77
* Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
88
*/
99

@@ -16,5 +16,8 @@ type Processor struct {
1616

1717
Weight int32 `json:"Weight,omitempty"`
1818

19-
Affinity uint64 `json:"Affinity,omitempty"`
19+
// GroupAffinities specifies the processor group affinity for the container.
20+
// Each entry pins the container to the given set of processors within a processor group.
21+
// Requires Windows Server 2022 (build 20348) or later.
22+
GroupAffinities []ProcessorGroupAffinity `json:"GroupAffinities,omitempty"`
2023
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* HCS API
3+
*
4+
* No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
5+
*
6+
* API version: 2.4
7+
* Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
8+
*/
9+
10+
package hcsschema
11+
12+
// ProcessorGroupAffinity specifies a processor group and an affinity mask within
13+
// that group for an HCS container. It mirrors the Win32 GROUP_AFFINITY structure.
14+
// Requires Windows Server 2022 (build 20348) or later.
15+
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/miniport/ns-miniport-_group_affinity
16+
type ProcessorGroupAffinity struct {
17+
// Mask is the bitmask of processors within Group.
18+
Mask uint64 `json:"Mask,omitempty"`
19+
// Group is the processor group number (0-based). Group 0 is the most common
20+
// value; the tag intentionally omits omitempty so that group 0 is not
21+
// silently dropped from the JSON sent to HCS.
22+
Group uint16 `json:"Group"`
23+
}

internal/hcsoci/hcsdoc_wcow.go

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ import (
3131

3232
const createContainerSubdirectoryForProcessDumpSuffix = "{container_id}"
3333

34+
// Sentinel errors returned by ConvertCPUAffinity.
35+
var (
36+
// ErrCPUAffinityMultipleGroupsNotSupported is returned when multiple processor-group
37+
// affinity entries are requested on a host older than Windows Server 2022 (build 20348),
38+
// which does not support multi-group affinity for job object silos.
39+
// On Windows Server 2022+, multiple processor groups are fully supported.
40+
ErrCPUAffinityMultipleGroupsNotSupported = errors.New("cpu affinity with multiple processor groups requires Windows Server 2022 or later")
41+
// ErrCPUAffinityNonZeroGroupNotSupported is returned when a non-zero processor group is
42+
// requested on a host older than Windows Server 2022 (build 20348).
43+
// On Windows Server 2022+, non-zero processor groups are fully supported.
44+
ErrCPUAffinityNonZeroGroupNotSupported = errors.New("cpu affinity with a non-zero processor group requires Windows Server 2022 or later")
45+
// ErrCPUAffinityMaskZero is returned when an affinity entry has a zero bitmask,
46+
// which would select no processors and is always invalid.
47+
ErrCPUAffinityMaskZero = errors.New("cpu affinity mask must be non-zero")
48+
)
49+
3450
// A simple wrapper struct around the container mount configs that should be added to the
3551
// container.
3652
type mountsConfig struct {
@@ -97,26 +113,36 @@ func createMountsConfig(ctx context.Context, coi *createOptionsInternal) (*mount
97113
// ConvertCPUAffinity handles the logic of converting and validating the container's CPU affinity
98114
// specified in the OCI spec to what HCS expects.
99115
//
100-
// Returns the CPU affinity bitmask (0 if not specified) and any validation error.
101-
// Phase 2 limitations:
102-
// - Multiple affinity entries are rejected
103-
// - Non-zero processor groups are rejected
104-
func ConvertCPUAffinity(spec *specs.Spec) (uint64, error) {
116+
// Returns the validated affinity entries (nil if not specified) and any validation error.
117+
// Multiple processor groups and non-zero group numbers require Windows Server 2022
118+
// (build 20348) or later; on older hosts only a single entry for group 0 is accepted.
119+
func ConvertCPUAffinity(spec *specs.Spec) ([]specs.WindowsCPUGroupAffinity, error) {
105120
if spec.Windows == nil || spec.Windows.Resources == nil || spec.Windows.Resources.CPU == nil || len(spec.Windows.Resources.CPU.Affinity) == 0 {
106-
return 0, nil
121+
return nil, nil
107122
}
108123

109124
affinity := spec.Windows.Resources.CPU.Affinity
110-
if len(affinity) != 1 {
111-
return 0, fmt.Errorf("cpu affinity with multiple processor groups is not supported")
112-
}
113-
if affinity[0].Group != 0 {
114-
return 0, fmt.Errorf("cpu affinity processor group %d is not supported", affinity[0].Group)
125+
126+
// Zero masks are never valid regardless of OS version.
127+
for i, a := range affinity {
128+
if a.Mask == 0 {
129+
return nil, fmt.Errorf("%w: entry %d has zero mask", ErrCPUAffinityMaskZero, i)
130+
}
115131
}
116-
if affinity[0].Mask == 0 {
117-
return 0, fmt.Errorf("cpu affinity mask must be non-zero")
132+
133+
// Determine whether multi-group features are needed: either multiple entries,
134+
// or a single entry targeting a non-zero processor group.
135+
multiGroup := len(affinity) > 1 || affinity[0].Group != 0
136+
137+
// Multiple processor groups are only supported on Windows Server 2022+.
138+
if multiGroup && osversion.Build() < osversion.LTSC2022 {
139+
if len(affinity) > 1 {
140+
return nil, fmt.Errorf("%w: %d entries", ErrCPUAffinityMultipleGroupsNotSupported, len(affinity))
141+
}
142+
return nil, fmt.Errorf("%w: group %d", ErrCPUAffinityNonZeroGroupNotSupported, affinity[0].Group)
118143
}
119-
return affinity[0].Mask, nil
144+
145+
return affinity, nil
120146
}
121147

122148
// ConvertCPULimits handles the logic of converting and validating the containers CPU limits
@@ -209,6 +235,7 @@ func createWindowsContainerDocument(ctx context.Context, coi *createOptionsInter
209235
return nil, nil, err
210236
}
211237

238+
// Validate and retrieve CPU affinity from the spec.
212239
cpuAffinity, err := ConvertCPUAffinity(coi.Spec)
213240
if err != nil {
214241
return nil, nil, err
@@ -262,12 +289,22 @@ func createWindowsContainerDocument(ctx context.Context, coi *createOptionsInter
262289
v1.ProcessorMaximum = int64(cpuLimit)
263290
v1.ProcessorWeight = uint64(cpuWeight)
264291

265-
v2Container.Processor = &hcsschema.Processor{
266-
Count: cpuCount,
267-
Maximum: cpuLimit,
268-
Weight: cpuWeight,
269-
Affinity: cpuAffinity,
292+
v2Processor := &hcsschema.Processor{
293+
Count: cpuCount,
294+
Maximum: cpuLimit,
295+
Weight: cpuWeight,
296+
}
297+
if len(cpuAffinity) > 0 {
298+
groupAffs := make([]hcsschema.ProcessorGroupAffinity, len(cpuAffinity))
299+
for i, a := range cpuAffinity {
300+
groupAffs[i] = hcsschema.ProcessorGroupAffinity{
301+
Mask: a.Mask,
302+
Group: uint16(a.Group),
303+
}
304+
}
305+
v2Processor.GroupAffinities = groupAffs
270306
}
307+
v2Container.Processor = v2Processor
271308

272309
// Memory Resources
273310
memoryMaxInMB := oci.ParseAnnotationsMemory(ctx, coi.Spec, annotations.ContainerMemorySizeInMB, 0)

internal/hcsoci/hcsdoc_wcow_test.go

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
package hcsoci
44

55
import (
6-
"strings"
6+
"errors"
77
"testing"
88

99
specs "github.com/opencontainers/runtime-spec/specs-go"
10+
11+
"github.com/Microsoft/hcsshim/osversion"
1012
)
1113

1214
func TestConvertCPUAffinity_Group0MaskSet(t *testing.T) {
@@ -22,16 +24,16 @@ func TestConvertCPUAffinity_Group0MaskSet(t *testing.T) {
2224
},
2325
}
2426

25-
affinity, err := ConvertCPUAffinity(s)
27+
affinities, err := ConvertCPUAffinity(s)
2628
if err != nil {
2729
t.Fatalf("ConvertCPUAffinity failed: %v", err)
2830
}
29-
if affinity != 0x3 {
30-
t.Fatalf("unexpected cpu affinity: got %d want %d", affinity, uint64(0x3))
31+
if len(affinities) != 1 || affinities[0].Mask != 0x3 || affinities[0].Group != 0 {
32+
t.Fatalf("unexpected cpu affinity: got %v", affinities)
3133
}
3234
}
3335

34-
func TestConvertCPUAffinity_MultiGroupRejected(t *testing.T) {
36+
func TestConvertCPUAffinity_MultiGroup(t *testing.T) {
3537
s := &specs.Spec{
3638
Windows: &specs.Windows{
3739
Resources: &specs.WindowsResources{
@@ -45,16 +47,26 @@ func TestConvertCPUAffinity_MultiGroupRejected(t *testing.T) {
4547
},
4648
}
4749

48-
_, err := ConvertCPUAffinity(s)
49-
if err == nil {
50-
t.Fatal("expected error for multiple affinity entries")
51-
}
52-
if !strings.Contains(err.Error(), "multiple processor groups") {
53-
t.Fatalf("unexpected error: %v", err)
50+
affinities, err := ConvertCPUAffinity(s)
51+
if osversion.Build() >= osversion.LTSC2022 {
52+
// Multi-group is supported on WS2022+.
53+
if err != nil {
54+
t.Fatalf("expected success for multi-group on WS2022+, got: %v", err)
55+
}
56+
if len(affinities) != 2 {
57+
t.Fatalf("expected 2 affinity entries, got %d", len(affinities))
58+
}
59+
} else {
60+
if err == nil {
61+
t.Fatal("expected error for multiple affinity entries on pre-WS2022")
62+
}
63+
if !errors.Is(err, ErrCPUAffinityMultipleGroupsNotSupported) {
64+
t.Fatalf("unexpected error: %v", err)
65+
}
5466
}
5567
}
5668

57-
func TestConvertCPUAffinity_NonZeroGroupRejected(t *testing.T) {
69+
func TestConvertCPUAffinity_NonZeroGroup(t *testing.T) {
5870
s := &specs.Spec{
5971
Windows: &specs.Windows{
6072
Resources: &specs.WindowsResources{
@@ -67,12 +79,22 @@ func TestConvertCPUAffinity_NonZeroGroupRejected(t *testing.T) {
6779
},
6880
}
6981

70-
_, err := ConvertCPUAffinity(s)
71-
if err == nil {
72-
t.Fatal("expected error for non-zero affinity group")
73-
}
74-
if !strings.Contains(err.Error(), "processor group") {
75-
t.Fatalf("unexpected error: %v", err)
82+
affinities, err := ConvertCPUAffinity(s)
83+
if osversion.Build() >= osversion.LTSC2022 {
84+
// Non-zero group is supported on WS2022+.
85+
if err != nil {
86+
t.Fatalf("expected success for non-zero group on WS2022+, got: %v", err)
87+
}
88+
if len(affinities) != 1 || affinities[0].Group != 1 {
89+
t.Fatalf("unexpected affinity: got %v", affinities)
90+
}
91+
} else {
92+
if err == nil {
93+
t.Fatal("expected error for non-zero affinity group on pre-WS2022")
94+
}
95+
if !errors.Is(err, ErrCPUAffinityNonZeroGroupNotSupported) {
96+
t.Fatalf("unexpected error: %v", err)
97+
}
7698
}
7799
}
78100

@@ -93,7 +115,7 @@ func TestConvertCPUAffinity_ZeroMaskRejected(t *testing.T) {
93115
if err == nil {
94116
t.Fatal("expected error for zero affinity mask")
95117
}
96-
if !strings.Contains(err.Error(), "mask must be non-zero") {
118+
if !errors.Is(err, ErrCPUAffinityMaskZero) {
97119
t.Fatalf("unexpected error: %v", err)
98120
}
99121
}
@@ -137,12 +159,12 @@ func TestConvertCPUAffinity_NoAffinity(t *testing.T) {
137159

138160
for _, tc := range testCases {
139161
t.Run(tc.name, func(t *testing.T) {
140-
affinity, err := ConvertCPUAffinity(tc.spec)
162+
affinities, err := ConvertCPUAffinity(tc.spec)
141163
if err != nil {
142164
t.Fatalf("ConvertCPUAffinity failed: %v", err)
143165
}
144-
if affinity != 0 {
145-
t.Fatalf("expected zero affinity, got %d", affinity)
166+
if len(affinities) != 0 {
167+
t.Fatalf("expected empty affinities, got %v", affinities)
146168
}
147169
})
148170
}

internal/jobcontainers/oci.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ package jobcontainers
44

55
import (
66
"context"
7-
"fmt"
87

98
"github.com/Microsoft/hcsshim/internal/hcsoci"
109
"github.com/Microsoft/hcsshim/internal/jobobject"
@@ -41,19 +40,21 @@ func specToLimits(ctx context.Context, cid string, s *specs.Spec) (*jobobject.Jo
4140
return nil, err
4241
}
4342

44-
var cpuAffinity uint64
45-
if s.Windows != nil && s.Windows.Resources != nil && s.Windows.Resources.CPU != nil && len(s.Windows.Resources.CPU.Affinity) > 0 {
46-
affinity := s.Windows.Resources.CPU.Affinity
47-
if len(affinity) != 1 {
48-
return nil, fmt.Errorf("cpu affinity with multiple processor groups is not supported")
49-
}
50-
if affinity[0].Group != 0 {
51-
return nil, fmt.Errorf("cpu affinity processor group %d is not supported", affinity[0].Group)
52-
}
53-
if affinity[0].Mask == 0 {
54-
return nil, fmt.Errorf("cpu affinity mask must be non-zero")
43+
// Validate and retrieve CPU affinity using the shared helper, which enforces the
44+
// OS version gate for multi-group support (WS2022+).
45+
affinities, err := hcsoci.ConvertCPUAffinity(s)
46+
if err != nil {
47+
return nil, err
48+
}
49+
var groupAffinities []jobobject.GroupAffinity
50+
if len(affinities) > 0 {
51+
groupAffinities = make([]jobobject.GroupAffinity, len(affinities))
52+
for i, a := range affinities {
53+
groupAffinities[i] = jobobject.GroupAffinity{
54+
Mask: a.Mask,
55+
Group: uint16(a.Group),
56+
}
5557
}
56-
cpuAffinity = affinity[0].Mask
5758
}
5859

5960
realCPULimit, realCPUWeight := uint32(cpuLimit), uint32(cpuWeight)
@@ -77,7 +78,7 @@ func specToLimits(ctx context.Context, cid string, s *specs.Spec) (*jobobject.Jo
7778
return &jobobject.JobLimits{
7879
CPULimit: realCPULimit,
7980
CPUWeight: realCPUWeight,
80-
CPUAffinity: cpuAffinity,
81+
GroupAffinities: groupAffinities,
8182
MaxIOPS: maxIops,
8283
MaxBandwidth: maxBandwidth,
8384
MemoryLimitInBytes: memLimitMB * memory.MiB,

0 commit comments

Comments
 (0)