Dagu v2.15.3 - Scheduler Memory And Precondition Skip


Dagu v2.15.3 shipped on 25 August 2026. The patch bounds scheduler queue scans so worker outages no longer pin large run snapshots in the shared status cache. Child invocation steps whose root preconditions fail are now skipped instead of failing the parent workflow.

The full release notes and downloads are on the GitHub release page.

Distributed workers that drop off used to leave the scheduler in a bad loop. Each processing cycle scanned every item in every active queue. Worker eligibility checks then pulled cached run status for each queued run. The shared status cache kept those snapshots for up to an hour even though the scheduler needed them for a single eligibility decision. An unavailable worker kept the runs queued, so the scan repeated and retained memory grew with queue depth.

#2616 caps that path. A queue cycle now scans at most 100 items. The queue head is always inspected first. The rest of the window rotates with a persistent cursor so later items still get a turn. Eligibility and reservation checks read status through ReadStatusUncached and do not populate the shared cache. Condition updates look up a single queue item with GetByItemID instead of loading the full queue. Concurrent queue scans are capped at eight. If a mutation invalidates a scan cursor, the processor recovers instead of holding a stale page.

#2617 then parks a completed bounded page until something relevant changes: queue membership, worker routing, or capacity. Parked generations are rechecked on the existing scheduler cadence without rereading queue pages, statuses, or DAG snapshots. The queue store now exposes a Revision value so membership changes are cheap to detect. Unchanged dispatch failures wait for a generation change or the scheduler fallback interval.

Operators who run Dagu with a worker pool should see lower resident memory when workers are missing, and less CPU spent rescanning queues that cannot dispatch. The tradeoff is latency. A queued run behind a large backlog may wait for the cursor to rotate before the next eligibility check. Head fairness is preserved because the head is always in the window.

#2611 reported that an unmet sub-DAG precondition failed the parent workflow. #2614 changes the status contract.

When a child DAG aborts because a root precondition is not met, the child run stays aborted. The parent dag.run step is marked skipped. Other abort causes and evaluation errors still fail. The coordinator propagates the cause as RunStatus.PreconditionNotMet so local and distributed execution share the same mapping.

Workflows that already use continue_on: skipped can now proceed past a blocked child instead of stopping. Workflows that treated any child abort as a hard parent failure will see a skip where they used to see an error. That is the correctness fix and the behavior change in the same patch.

server and start-all used to swallow a second interrupt during teardown. After the first SIGINT, deferred cleanup could block, and a second SIGINT did nothing. #2612 restores default signal handling before long running cleanup so a second interrupt terminates the process. Local agent session cleanup now uses a shutdown context of 10 seconds. Teardown order is explicit: resource and tunnel services, then license manager, then agent session cleanup.

That matters on Kubernetes. A pod that ignored a second signal sat in Terminating until the grace period expired and the kubelet sent SIGKILL. The process can now exit on the second interrupt instead of waiting on a stuck closer.

#2615 is a UI change with an operator payoff. Audit log rows no longer truncate the payload. An entry drawer shows event, identity, access, resource, and trace fields, plus a raw JSON view that can be copied. Filters that were uncommon sit behind a compact disclosure. The Start DAG dialog layout in #2613 is a UI only change and does not alter the run contract.

This is not marked as a breaking release. Three behavior changes still affect operators.

Custom persistence layers that implement dagrun.Attempt need ReadStatusUncached. Implementations that do not cache can forward to ReadStatus. Custom queue.QueueStore types need GetByItemID and a ListCursor that returns pagination.ErrInvalidCursor when a cursor is invalid. #2617 also adds Revision on the queue store.

YAML DAGs that call a child with a root precondition should expect a skipped parent step when that precondition fails. Genuine child aborts still fail the parent.

A second SIGINT to server or start-all now kills the process. Do not assume blocked cleanup will finish after the second signal.