Skip to content

a cross-fiber dependency cycle deadlocks under fan-out instead of erroring #41

Description

@wyattgill9

A dependency cycle reports a clean error at --workers 1 and hangs forever at --workers 16.

$ cat cyc.nix
let
  work = n: builtins.foldl' (a: b: a + b) 0 (builtins.genList (i: i) n);
  xs = builtins.genList (i:
        if i == 17 then (work 20000) + (builtins.elemAt xs 33)
        else if i == 33 then (work 20000) + (builtins.elemAt xs 17)
        else work 30000) 64;
in builtins.deepSeq xs 1

Eight runs each, 15s timeout (124 is the timeout, 1 is the correct error: RecursiveThunk):

workers=1               : 1 1 1 1 1 1 1 1
workers=16              : 124 1 124 124 1 124 124 1
workers=16 --no-fanout  : 1 1 1 1 1 1 1 1

The hang is a park, not a spin:

$ /usr/bin/time -l timeout 15 fix eval --workers 16 cyc.nix
15.01 real         0.11 user         0.07 sys

0.18s of CPU over 15s of wall clock, across 16 workers. Every worker is asleep.

Future.tryClaim (src/runtime/future.zig:210) detects recursion only when the same fiber re-enters its own claim:

.evaluating => {
    const c = self.claimer.load(.acquire);
    if (c == claimer) return .blackhole;
    return .busy;
},

.busy means enroll on the waiter list and yield. That is correct for genuine contention, but when a cycle is split across two fibers — which is exactly what the urgent list fan-out does here, since elements 17 and 33 land in different 16-item batches — fiber A claims 17 and parks on 33, fiber B claims 33 and parks on 17. There is no wait-for graph, no timeout and no claim stealing, so neither ever wakes.

--no-fanout restores the correct error, which is consistent with fan-out being what splits the cycle across fibers.

Two notes on why this survived the suite:

  • test-lang runs --workers 1 for all 471 cases, and bench_check defaults to 1, so nothing differential ever runs multi-worker.
  • model/FutureWait.tla models each Future in isolation. A wait cycle spanning two futures is outside the state space, so the TLA+ model cannot reach this state by construction.

fix 0.3.0 at 1f1a99cf, built -Doptimize=ReleaseSafe with Zig 0.16.0, darwin/aarch64. Reference implementation is Nix 2.34.8.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions