The walk for setuid programs is put in the idle I/O class so it yields to everything else, but only a scheduler that schedules by class (BFQ) honours that. The control-node's disks run mq-deadline, which does not, so the walk read at full priority. On 2026-10-11 the walk started at 01:27, right after the engine applied a new controller, and ran sixteen minutes; it kept two spinning disks saturated, the bus whose store shares them stalled for half a minute twice, and the controller lost its lease and restarted each time. The walk now also paces itself by PSI, which reads the same whatever the scheduler: before it reads a directory, while some task has waited on I/O for more than 10 % of the last ten seconds, it pauses two seconds at a time. It looks at most every 250 ms, pauses at most 30 s in a row and then reads one more directory, so a machine that is never quiet is walked slowly but still judged. Its own reads count in the pressure, so on an otherwise idle machine it holds the disk below the bound. Pressure it cannot read pauses nothing, and is said once. A paced walk finds what an unpaced one finds; a walk ended while it pauses is an incomplete walk, never one that found nothing.
135 lines
4.5 KiB
Go
135 lines
4.5 KiB
Go
package accounts
|
|
|
|
// The walk for setuid programs yields to a busy disk (novox/hq issue 433). The walk is put in the idle I/O class
|
|
// (idle_linux.go), but only a disk scheduler that schedules by class honours that (BFQ does). The control-node's
|
|
// disks run mq-deadline, which does not, and there the walk read at full priority: on 2026-10-11 it kept two
|
|
// spinning disks saturated for sixteen minutes, the bus whose store shares those disks stalled for half a minute
|
|
// twice, and the controller lost its lease each time. So the walk also paces itself by the machine's I/O pressure
|
|
// (PSI), which reads the same whatever the scheduler: while some task has waited on I/O for more than PaceAbove
|
|
// of the last ten seconds, the walk pauses before it reads the next directory.
|
|
//
|
|
// The walk's own reads count in that pressure, so on a machine where nothing else waits it slows itself to keep
|
|
// the disk below the bound; that is the yielding the idle class promised. A pause in a row is at most PaceAtMost,
|
|
// then one more directory is read: on a machine that is never quiet the walk is slow, but it still ends, so a
|
|
// machine is never left unjudged for good (the fault issue 361 repaired).
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// PaceAbove is the I/O pressure, as the percentage of the last ten seconds in which some task waited on I/O
|
|
// (PSI's "some avg10"), above which the walk pauses.
|
|
PaceAbove = 10.0
|
|
// PaceLook is how often the walk looks at the pressure while it walks.
|
|
PaceLook = 250 * time.Millisecond
|
|
// PacePause is one pause.
|
|
PacePause = 2 * time.Second
|
|
// PaceAtMost is the longest the walk pauses in a row before it reads one more directory.
|
|
PaceAtMost = 30 * time.Second
|
|
// ioPressureFile is where the kernel says the machine's I/O pressure.
|
|
ioPressureFile = "/proc/pressure/io"
|
|
)
|
|
|
|
// pacer is what the walk asks before it reads a directory. A nil pacer never pauses.
|
|
type pacer struct {
|
|
pressure func() (float64, error) // some avg10, in percent
|
|
sleep func(context.Context, time.Duration) error
|
|
now func() time.Time
|
|
logf func(format string, args ...any)
|
|
|
|
lastLook time.Time
|
|
saidBlind bool
|
|
}
|
|
|
|
// newPacer paces by the machine's own I/O pressure.
|
|
func newPacer() *pacer {
|
|
return &pacer{
|
|
pressure: func() (float64, error) { return readIOPressure(ioPressureFile) },
|
|
sleep: sleepCtx,
|
|
now: time.Now,
|
|
logf: log.Printf,
|
|
}
|
|
}
|
|
|
|
// yield pauses while the machine's I/O pressure is above PaceAbove, PacePause at a time and PaceAtMost in a row,
|
|
// looking at most every PaceLook. It ends early only with ctx, whose error ends the walk as incomplete. Pressure
|
|
// that cannot be read pauses nothing, and is said once: the walk then yields only where the idle class does.
|
|
func (pc *pacer) yield(ctx context.Context) error {
|
|
if pc == nil || pc.pressure == nil {
|
|
return nil
|
|
}
|
|
if !pc.lastLook.IsZero() && pc.now().Sub(pc.lastLook) < PaceLook {
|
|
return nil
|
|
}
|
|
defer func() { pc.lastLook = pc.now() }()
|
|
for waited := time.Duration(0); waited < PaceAtMost; waited += PacePause {
|
|
p, err := pc.pressure()
|
|
if err != nil {
|
|
if !pc.saidBlind && pc.logf != nil {
|
|
pc.logf("the walk for setuid programs cannot read the machine's I/O pressure, so it does not pace "+
|
|
"itself and yields only where the disk's scheduler honours the idle class: %v", err)
|
|
}
|
|
pc.saidBlind = true
|
|
return nil
|
|
}
|
|
if p <= PaceAbove {
|
|
return nil
|
|
}
|
|
if err := pc.sleep(ctx, PacePause); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// readIOPressure is the "some avg10" of a PSI file: the percentage of the last ten seconds in which some task
|
|
// waited on I/O.
|
|
func readIOPressure(path string) (float64, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return parseIOPressure(b)
|
|
}
|
|
|
|
func parseIOPressure(b []byte) (float64, error) {
|
|
sc := bufio.NewScanner(bytes.NewReader(b))
|
|
for sc.Scan() {
|
|
fields := strings.Fields(sc.Text())
|
|
if len(fields) == 0 || fields[0] != "some" {
|
|
continue
|
|
}
|
|
for _, f := range fields[1:] {
|
|
if v, ok := strings.CutPrefix(f, "avg10="); ok {
|
|
p, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("the I/O pressure's avg10 is not a number: %q", v)
|
|
}
|
|
return p, nil
|
|
}
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("the I/O pressure names no \"some\" line with avg10")
|
|
}
|
|
|
|
// sleepCtx waits d, or until ctx ends.
|
|
func sleepCtx(ctx context.Context, d time.Duration) error {
|
|
t := time.NewTimer(d)
|
|
defer t.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-t.C:
|
|
return nil
|
|
}
|
|
}
|