Skip to content
Merged
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
52 changes: 49 additions & 3 deletions cmd/plugins/topology-aware/policy/irq-affinity.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ package topologyaware

import (
"fmt"
"strconv"

"github.com/containers/nri-plugins/pkg/irq"
"github.com/containers/nri-plugins/pkg/topology"
"github.com/containers/nri-plugins/pkg/utils/cpuset"
"sigs.k8s.io/yaml"
)

type IrqAffinity struct {
Claim []string `json:"claim,omitempty"`
Mask []string `json:"mask,omitempty"`
Mode IrqMode `json:"mode,omitempty"`
Claim []string `json:"claim,omitempty"`
Devices []string `json:"devices,omitempty"`
Mask []string `json:"mask,omitempty"`
Mode IrqMode `json:"mode,omitempty"`
}

type IrqMode string
Expand Down Expand Up @@ -60,6 +63,49 @@ func parseIrqAffinity(raw []byte) (*IrqAffinity, error) {
return parsed, nil
}

func addIrqAffinityForHints(a *IrqAffinity, hints topology.Hints) error {
Comment thread
klihub marked this conversation as resolved.
if len(hints) == 0 || a == nil || len(a.Devices) == 0 {
return nil
}

if err := irq.ValidateAllowedPatterns(a.Devices); err != nil {
return fmt.Errorf("invalid IRQ affinity devices pattern: %w", err)
}

for source, h := range hints {
for _, num := range h.IRQs {
irq, err := irq.Interrupt(num)
switch {
case err != nil:
log.Errorf("irq: skipping %s-hinted IRQ %d: %v", source, num, err)
continue
case !irq.IsAllowed():
log.Warnf("irq: skipping denied %s-hinted IRQ %d", source, num)
continue
}

matched := false
for _, p := range a.Devices {
if irq.Match(p) {
Comment thread
klihub marked this conversation as resolved.
matched = true
break
}
}
if !matched {
log.Debugf("irq: skipping unmatched %s-hinted IRQ %d (%q)",
source, num, irq.Description())
continue
}

log.Infof("irq: claim matching %s-hinted IRQ %d (%q)",
source, num, irq.Description())
a.Claim = append(a.Claim, strconv.Itoa(irq.Num()))
}
}

return nil
Comment thread
klihub marked this conversation as resolved.
}

func (p *policy) irqCpus(hwIrq *irq.Irq) (preMask, claim, mask cpuset.CPUSet) {
preMask, claim, mask = cpuset.New(), cpuset.New(), cpuset.New()
for _, g := range p.allocations.grants {
Expand Down
4 changes: 4 additions & 0 deletions cmd/plugins/topology-aware/policy/pod-preferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@ func irqAffinityPreference(ctr cache.Container) (*IrqAffinity, bool, error) {

switch {
case qos == corev1.PodQOSGuaranteed:
err := addIrqAffinityForHints(a, ctr.GetTopologyHints())
if err != nil {
return nil, scope == cache.ContainerScopedAnnotation, err
}
return a, scope == cache.ContainerScopedAnnotation, nil
case scope == cache.ContainerScopedAnnotation:
return nil, true, fmt.Errorf("invalid IRQ affinity, QoS class %v is not Guaranteed", qos)
Expand Down
5 changes: 4 additions & 1 deletion docs/resource-policy/policy/topology-aware.md
Original file line number Diff line number Diff line change
Expand Up @@ -739,14 +739,17 @@ Containers eligible for exclusive CPU allocation can be annotated with IRQ
tuning to claim selected IRQs, mask selected IRQs, or do both, using the
`irq-affinity.resource-policy.nri.io` annotation key.

The annotation has 3 fields:
The annotation has 4 fields:
**`claim`** (list of strings)
- Lists IRQs handled by the exclusive CPUs allocated to the container.
- Each item refers to IRQs either by an exact number, or by a pattern
matching last columns in `/proc/interrupts`. Supports wildcards, for
instance "*nvme*".
- The affinity of a claimed IRQ is set to the union of CPUs of all
containers that claim it.
**`devices`** (list of strings)
Comment thread
klihub marked this conversation as resolved.
- The same as `claim`, but listed IRQs match only devices assigned to
the container instead of all devices in the system.
**`mask`** (list of strings)
- Lists IRQs which should not be handled by the exclusive CPUs allocated
to the container.
Expand Down
19 changes: 19 additions & 0 deletions pkg/irq/irq-cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,25 @@ func (c *irqCache) forEachInterrupt(fn func(*Irq) error, allow []string) error {
return nil
}

// irqByNum returns the interrupt for the given IRQ number.
func (c *irqCache) irqByNum(num int, allow []string) (*Irq, error) {
infos, err := c.interruptInfo()
if err != nil {
return nil, err
}

info, ok := infos[num]
if !ok {
return nil, fmt.Errorf("%w: irq %d not found", ErrNoSuchInterrupt, num)
}

return &Irq{
num: info.num,
description: info.description,
denied: !isAllowedInterruptBy(info.description, allow),
}, nil
}

// affinityOf returns the CPUs in the affinity of the given interrupt.
// The affinity is read from procfs only until it is known, and
// affinities set through the cache are visible before they have been
Expand Down
2 changes: 1 addition & 1 deletion pkg/irq/irq-cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ func TestAllowedPatternsCalculatedPerCall(t *testing.T) {

denied := map[int]bool{}
if err := ForEachInterrupt(func(irq *Irq) error {
denied[irq.Num()] = !irq.isAllowed()
denied[irq.Num()] = !irq.IsAllowed()
return nil
}); err != nil {
t.Fatalf("ForEachInterrupt() failed: %v", err)
Expand Down
16 changes: 14 additions & 2 deletions pkg/irq/irq.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ var (
// ErrDeniedInterrupt is the error returned for attempts to reference or control
// globally disallowed interrupts.
ErrDeniedInterrupt = errors.New("denied interrupt")
// ErrNoSuchInterrupt is the error returned for attempts to look up a
// nonexistent interrupt by number.
ErrNoSuchInterrupt = errors.New("no such interrupt")
)

// SetProcRoot sets the procfs root directory and proc mountpoint. All
Expand Down Expand Up @@ -176,6 +179,12 @@ func Interrupts() ([]*Irq, error) {
return allowedInterrupts(allowed)
}

// Interrupt returns the IRQ corresponding to the given interrupt number.
func Interrupt(num int) (*Irq, error) {
irq, err := cache.irqByNum(num, allowed)
return irq, err
}

// allowedInterrupts collects and returns the numbered interrupts
// listed in /proc/interrupts which can be controlled by this package
// according to the given allow patterns.
Expand Down Expand Up @@ -268,7 +277,10 @@ func (irq *Irq) Match(pattern string) bool {
return err == nil && match
}

func (irq *Irq) isAllowed() bool {
// IsAllowed returns true if this interrupt may be controlled
// by the package according to the allow patterns in effect
// when the Irq was introspected.
func (irq *Irq) IsAllowed() bool {
return !irq.denied
}

Expand All @@ -283,7 +295,7 @@ func (irq *Irq) AffinityCpus() (cpuset.CPUSet, error) {
// While writes are blocked, the affinity is only buffered and write
// errors are logged instead of being returned.
func (irq *Irq) SetAffinityCpus(cpus cpuset.CPUSet) error {
if !irq.isAllowed() {
if !irq.IsAllowed() {
return fmt.Errorf("%w: refusing to set affinity of irq %d", ErrDeniedInterrupt, irq.num)
}
if cpus.IsEmpty() {
Expand Down
60 changes: 60 additions & 0 deletions pkg/irq/irq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package irq

import (
"errors"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -80,6 +81,65 @@ func TestInterruptsAndMatch(t *testing.T) {
}
}

func TestInterrupt(t *testing.T) {
dir := t.TempDir()
SetProcRoot(dir)
if err := os.Mkdir(filepath.Join(dir, "proc"), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "proc", "interrupts"), []byte(sampleInterrupts), 0644); err != nil {
t.Fatal(err)
}

for _, tc := range []struct {
name string
num int
description string
err error
}{
{
name: "IRQ 1, i8042",
num: 1,
description: "IR-IO-APIC 1-edge i8042",
err: nil,
},
{
name: "IRQ 9, acpi",
num: 9,
description: "IR-IO-APIC 9-fasteoi acpi",
err: nil,
},
{
name: "IRQ 16, processor_thermal_device_pci",
num: 16,
description: "IR-IO-APIC 16-fasteoi i801_smbus, processor_thermal_device_pci",
err: nil,
},
{
name: "non-existent IRQ 666",
num: 666,
description: "non-existent IRQ 666",
err: ErrNoSuchInterrupt,
},
} {
t.Run(tc.name, func(t *testing.T) {
irq, err := Interrupt(tc.num)
switch {
case err != nil && tc.err == nil:
t.Fatalf("unexpected failure for Interrupt(%d): %v", tc.num, err)
case err != nil && !errors.Is(err, tc.err):
t.Fatalf("wrong error for Interrupt(%d): %v, expecting %v",
tc.num, err, tc.err)
case err == nil:
if irq.Num() != tc.num || irq.Description() != tc.description {
t.Fatalf("Interrupt(%d) = %v, want %d, %q", tc.num,
irq, tc.num, tc.description)
}
}
})
}
}

func TestAffinityReadWrite(t *testing.T) {
dir := t.TempDir()
SetProcRoot(dir)
Expand Down
Loading
Loading