Current behaviour #
A workflow's timeout clock starts before it waits for a slot, so time spent queued counts against the workflow timeout. On a busy spindle a workflow can be killed as timeout without ever running for long: on ci.klbr.net a test.yml waited 39 minutes behind a 40-minute job and timed out a minute after it started.
In spindle/engine/engine.go, context.WithTimeout(wfCtx, workflowTimeout) is created at the top of the per-workflow goroutine. The quota reservation (qm.Acquire(wfCtx, …)) and the slot wait (AcquireWorkflowSlot(wfCtx, …, Wait)) both run under that context afterwards. The engine already tells the two apart in its log: the workflow finished line has wait_seconds and run_seconds.
Change #
Start the timeout once the slot is held, so the budget covers running time only. User cancellation must keep working while a workflow is queued, so the cancel context should stay outside the timeout. Waiting could get its own, separate bound if unbounded queueing is a concern.
Done when #
A workflow that queues longer than the workflow timeout, then runs for less than it, finishes successfully. A test with a slotter that blocks past the timeout before granting the slot shows this.
Found while running a self-hosted spindle (ci.klbr.net). Issue found and reported by Opus 5.5.