space: the diff's clock runs inside sudo, so zfs is killed too
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
314a021a3e
commit
ad88a48c01
@@ -356,10 +356,12 @@ something long in one, switch away and back, refresh — it's all still there.
|
|||||||
snapshots, the scan records the newest snapshot of each dataset at start (`bases`), and the
|
snapshots, the scan records the newest snapshot of each dataset at start (`bases`), and the
|
||||||
next one runs `zfs diff -H -F <base>` per dataset: only the directories it names are
|
next one runs `zfs diff -H -F <base>` per dataset: only the directories it names are
|
||||||
read, their ancestors kept but looked into, everything else kept wholesale without a
|
read, their ancestors kept but looked into, everything else kept wholesale without a
|
||||||
stat. The diffs run side by side and one is given four minutes (its process group is
|
stat. The diffs run side by side and one is given three minutes — `timeout -s KILL`
|
||||||
killed, sudo and zfs both), after which that dataset is walked by mtime instead — on
|
inside sudo, since Debian's sudo gives the command its own session and a process-group
|
||||||
`tank`, `immich` changes constantly and its diff ran past twenty minutes the first time,
|
kill from outside never reached zfs — after which that dataset is walked by mtime
|
||||||
longer than walking it. Elsewhere (ext4, or a dataset with no snapshot) a directory whose mtime — to the
|
instead. On `tank` a diff is metadata-bound like the walk: 8 s to 3 min per dataset for
|
||||||
|
a handful of changes, and `immich`, which changes constantly, ran past twenty minutes
|
||||||
|
the first time. A refresh of `tank` took 4 min 19 s against 7 min for the full scan. Elsewhere (ext4, or a dataset with no snapshot) a directory whose mtime — to the
|
||||||
nanosecond — is unchanged keeps its list and only its kept files are stat'ed again, since
|
nanosecond — is unchanged keeps its list and only its kept files are stat'ed again, since
|
||||||
a file can grow without its directory noticing. A **cancelled** scan keeps what it walked,
|
a file can grow without its directory noticing. A **cancelled** scan keeps what it walked,
|
||||||
marks the folders it never got to (`partial`, "not yet read" in the tree) and **Resume**
|
marks the folders it never got to (`partial`, "not yet read" in the tree) and **Resume**
|
||||||
|
|||||||
@@ -571,16 +571,19 @@ func newestSnapshot(ctx context.Context, dataset string) string {
|
|||||||
// zfsDiff lists what changed in a dataset since a snapshot: every changed, added, removed
|
// zfsDiff lists what changed in a dataset since a snapshot: every changed, added, removed
|
||||||
// or renamed path, with the directory whose listing that touches.
|
// or renamed path, with the directory whose listing that touches.
|
||||||
func zfsDiff(ctx context.Context, snapshot, mount string) ([]change, error) {
|
func zfsDiff(ctx context.Context, snapshot, mount string) ([]change, error) {
|
||||||
// a diff slower than this is not worth waiting for: the walk by mtime is bounded
|
// A diff slower than this is not worth waiting for: the walk by mtime is bounded. The
|
||||||
ctx, cancel := context.WithTimeout(ctx, 4*time.Minute)
|
// clock runs inside sudo — `timeout` is zfs's parent and kills it as root — because sudo
|
||||||
|
// on Debian gives the command a session of its own, out of reach of our process group;
|
||||||
|
// the context is only a backstop.
|
||||||
|
const patience = 3 * time.Minute
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, patience+30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
args := priv.Wrap("zfs", "diff", "-H", "-F", snapshot)
|
args := priv.Wrap("timeout", "-s", "KILL", strconv.Itoa(int(patience.Seconds())), "zfs", "diff", "-H", "-F", snapshot)
|
||||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
||||||
// on timeout the whole process group goes, not just sudo: zfs itself would otherwise run
|
|
||||||
// on holding the pipe, and Wait would sit there with it
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
cmd.Cancel = func() error { return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) }
|
cmd.Cancel = func() error { return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) }
|
||||||
cmd.WaitDelay = 5 * time.Second
|
cmd.WaitDelay = 5 * time.Second
|
||||||
|
t0 := time.Now()
|
||||||
var stderr strings.Builder
|
var stderr strings.Builder
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
out, err := cmd.StdoutPipe()
|
out, err := cmd.StdoutPipe()
|
||||||
@@ -609,8 +612,8 @@ func zfsDiff(ctx context.Context, snapshot, mount string) ([]change, error) {
|
|||||||
}
|
}
|
||||||
if err := cmd.Wait(); err != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
msg := strings.TrimSpace(stderr.String())
|
msg := strings.TrimSpace(stderr.String())
|
||||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) || time.Since(t0) >= patience {
|
||||||
msg = "gave up after 4 min"
|
msg = "gave up after " + patience.String()
|
||||||
}
|
}
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
msg = err.Error()
|
msg = err.Error()
|
||||||
|
|||||||
Reference in New Issue
Block a user