Skip to content

CI: the ~55-min quickstart testnet step-8 hang recurred on #3765 attempt 4 — #3289's step-level timeout-minutes: 25 did not fire, and the job cancel destroyed the watchdog dump it was built to capture #3768

Description

@tomerweller

Finding

The ~55-min step-8 hang that #3272#3273#3286#3287#3289 chased across
three fixes recurred on PR #3765, attempt 4, with the same duration signature —
and #3289's step-level timeout-minutes: 25 did not fire. The job died on the
45-min job-level wall-clock instead, which is a cancel, so:

The instrumentation added specifically to capture this hang was destroyed by the
hang it was built to capture. That is the part worth acting on: this attempt
produced less evidence than the pre-#3289 attempts did.

.github/workflows/quickstart.yml, the design intent (lines ~455–465):

DIAGNOSTIC instrumentation (#3286): this shard also sets a TIGHT, testnet-only
step-level fail-fast bound via step_timeout_minutes: 25 … Two wrapper-level
fixes (#3273, #3287) both FAILED to bound the ~55-min step-8 hang, and a 45-min
JOB-level wall-clock is a cancel — so if: failure() never fires and the
diagnostics step never uploads. A step-level timeout-minutes kill instead marks
the STEP failed (distinct from a job cancel), so the upload step runs.

Attempt 4 is that exact failure mode, after the fix.

Evidence

Run 30329850375,
job 90210359873, PR #3765 (branch do/issue-3756, head 9b1f1314):

attempt=1  id=90187502593  failure     1640 s (27.3 min)  start 05:23:58Z
attempt=2  id=90191646140  failure     1070 s (17.8 min)  start 05:51:30Z
attempt=3  id=90196608534  failure     1600 s (26.7 min)  start 06:22:07Z
attempt=4  id=90210359873  cancelled   3300 s (55.0 min)  start 07:36:52Z   ← this one

Per-attempt step conclusions for Run probes through wrapper and the upload that
#3289 exists to enable:

attempt step secs step conclusion Upload diagnostics on failure
1 1590 (26.5 min) failure success
2 1036 (17.3 min) failure skipped
3 1544 (25.7 min) failure success
4 none none (never ran)

Attempts 1 and 3 land at 1590 / 1544 s — the 25-min (1500 s) step bound plus ~45–90 s
of overshoot — and the upload runs. That is #3289 working. Attempt 4 does not.

(Attempt 2 is a separate oddity: the step failed at 17.3 min, comfortably under the
bound, yet Upload diagnostics on failure shows skipped rather than running. I have
not chased that and am not claiming a cause; noting it so it isn't mistaken for the
attempt-4 signature.)

The 45-min job timeout fired, and needed the full 10-min hard grace

job started_at    2026-07-28T07:36:52Z
+ timeout-minutes: 45          → 08:21:52Z   (job cancel issued)
+ GitHub cancel grace (10 min) → 08:31:52Z
job completed_at  2026-07-28T08:31:52Z   ← exact match

Exact to the second, on both bounds. The 10-min overshoot past 45 min is the runner
being unable to terminate the step promptly — the same unkillability that explains
why the 25-min step bound produced nothing.

Step conclusions

step                                     secs   conclusion
Set up job                                  3   success
Run actions/checkout@v4                     4   success
Download quickstart image                  13   success
Load Docker image                          27   success
Checkout stellar/quickstart (tests)         1   success
Run actions/setup-go@v5                     1   success
Run quickstart container                    1   success
Run probes through wrapper                 --   None      ← never concluded
Upload diagnostics on failure              --   None      ← never ran
Cleanup                                    --   None
Post Run actions/setup-go@v5               --   None
Post Checkout stellar/quickstart (tests)   --   None
Post Run actions/checkout@v4                --   None

Run probes through wrapper has no completed_at and no conclusion. A step-level
timeout-minutes kill sets the step to failure; this step was never concluded at
all, so the 25-min bound did not act on it.

No logs exist

$ gh api repos/stellar-experimental/henyey/actions/jobs/90210359873/logs
<Error><Code>BlobNotFound</Code><Message>The specified blob does not exist.

Attempts 1–3 have logs; attempt 4 has none. So for the one attempt that reproduced
the full ~55-min hang, there is no step output, no probe timings, no soft-skip
markers, and no watchdog artifact.

Two candidate explanations, and what I ruled out

I can show the 25-min bound did not fire. I cannot show why, and there are no
attempt-4 logs to discriminate. The two live candidates:

  1. The step was uninterruptible. The runner issued the step kill at ~08:02:50 and
    could not complete it, so no conclusion was recorded; the job timeout at 08:21:52
    also could not complete, and only the 10-min hard grace terminated the runner. The
    10-min overshoot is direct evidence for the second half of this.
  2. Runner/infra fault independent of the hang.

Ruled out:

  • Concurrency cancel. quickstart.yml:113 sets cancel-in-progress: false, and
    gh run list shows exactly one quickstart run on do/issue-3756. Nothing
    superseded it.
  • Per-probe budget scaling with run_attempt. I hypothesised the wrapper's
    PROBE_TIMEOUT=$(( github.run_attempt * timeout_multiplier * 60 )) (line ~544)
    reached 4 × 4 × 60 = 960 s on attempt 4 and blew the step budget. Falsified
    lines 549–551 override it unconditionally on this shard:
    if [[ -n "$SHARD_PROBE_TIMEOUT" ]]; then
      PROBE_TIMEOUT="$SHARD_PROBE_TIMEOUT"
    fi
    with SHARD_PROBE_TIMEOUT: ${{ matrix.probe_timeout }} = 240. The per-probe
    budget is 240 s on every attempt. Attempt number is not the variable.

Why this matters beyond one red check

PR #3765 is MERGEABLE with every other check green — Clippy, Format,
Doctest, Build (release-ci), Test, all Analyze jobs, Monitor-Tick Skill Snippets, Quickstart Harness, Soroban Pin Check. The sole red is this shard.

#3765 offloads the peer_refresh arm behind #3756. On the production validator this
morning the event-loop park p95 is 23,951 ms against a 30,000 ms busy_timeout
ceiling, fitted at ~853 ms/day (out-of-sample validated to −17 ms), which puts the
p95 at the ceiling around 2026-08-03. So the shard is currently the gate in front
of a fix with a dated deadline, and it has now failed four times for reasons the
shard itself documents as environmental.

Bearing on #3290

#3290 was closed NOT_PLANNED with the reasoning that "the soft-skip resolves each
probe at its 240s bound, the step-level 25-min timeout (#3289) is a hard backstop."
I posted evidence yesterday that on attempts 1/3 the backstop fires but arrives with
no budget left for the remaining probes. Attempt 4 is a stronger statement:

attempts 1, 3 attempt 4
25-min step bound fires (step = 1590 / 1544 s) does not fire
diagnostics uploaded yes (success) no (never ran)
logs retained yes no (BlobNotFound)
terminated by step timeout-minutes job timeout-minutes + 10-min grace

A backstop that fires on 3 of 4 attempts is not a backstop for the case it was built
for. I'm recording that here rather than reopening #3290 — reversing a deliberate
NOT_PLANNED is an operator call, and this is input to it.

Suggested next steps

  1. A bound that does not depend on signalling the wedged step. Everything tried
    so far (timeout -k in the wrapper Soft-skip testnet quickstart probe timeouts to de-gate main #3273, orphan-pipe bounding Bound quickstart probe wrapper on orphaned stdout-pipe holders (#3286) #3287, step
    timeout-minutes Instrument testnet quickstart hang: fail-fast + process-tree watchdog (#3286) #3289) requires the hung process to be killable from inside the
    runner. A sidecar that writes the dump on a timer and uploads it independently of
    the step's exit — or docker kill from a separate step — would survive a job
    cancel.
  2. Lower WATCHDOG_DELAY and upload eagerly. The dump at ~07:58Z existed on disk
    for 34 minutes and was thrown away because the upload is gated on if: failure().
    Uploading from the watchdog itself (or on if: always()) would have salvaged it.
  3. Attempt 2 (17.8 min, failure) is under the step bound and therefore a
    different signature from 1/3/4 — worth reading its log for a genuine assertion
    failure before treating all four as environmental.
  4. Unblock Offload peer_refresh event-loop arm to fix 13-29s parking (#3756) #3765 on the Event loop parked 13-29s once per minute (28% duty-cycle loss): peer_refresh arm awaits inline, 69k-row peers table — recurrence of #3582 #3756 deadline. Whether that is de-gating the shard
    (CI testnet quickstart: cap cumulative probe wall-clock / decide whether the shard should gate at all (operator policy) #3290 option 2, the History Publish (Testnet): timed out waiting for first published checkpoint (regression 06-09..06-10) #3280 History-Publish precedent) or an admin merge is a policy
    call, but it should be made with the 08-03 date in view rather than by re-running a
    fifth time.

Not labelled urgent

CI, not a validator symptom, per the label policy. Flagging the coupling explicitly
because the blocked PR has a dated deadline.

Related: #3272, #3273, #3286, #3287, #3289, #3290 (the hang lineage), #3756 (the fix
this blocks), #3764 (same shard, same day).

Surfaced by /monitor-tick 2827.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions