Skip to content

fix: render-error hang — busy-state, stale-running, and cancel fixes (#34) - #35

Merged
ghirpara merged 8 commits into
masterfrom
fix/render-error-hang
Aug 15, 2026
Merged

fix: render-error hang — busy-state, stale-running, and cancel fixes (#34)#35
ghirpara merged 8 commits into
masterfrom
fix/render-error-hang

Conversation

@ghirpara

Copy link
Copy Markdown
Member

Summary

Fixes the render-error hang reported in #34: an Iray render that fails and pops a blocking modal dialog left the plugin permanently wedged, with no way for an HTTP client to detect or recover from it short of restarting DAZ Studio.

  • Clear render busy state on failed/errored renders. SceneEventBroker only wired enterBusy()/exitBusy() to aboutToRender/renderFinished, but a render that errors pops a blocking modal without ever calling renderFinished in some paths — leaving /execute permanently 503ing. Verified renderFinished(bool) fires as the guaranteed exit signal even on non-happy-path outcomes, and wired busy-state clearing off of it.
  • Auto-fail async requests stuck RUNNING behind a modal. Added AsyncRequestManager::failStaleRunning(), run from the existing 5-minute cleanup timer, marking any request RUNNING longer than ASYNC_STALE_RUNNING_TIMEOUT_MIN (30 min default) as FAILED with a diagnostic message, so GET /requests/:id no longer polls "running" forever. markCompleted() now no-ops for requests already in a terminal state, so a late real completion can't flip a timed-out request back.
  • Make cancel authoritative. cancelJson()/cancelRenderJson() previously only transitioned QUEUED requests to CANCELLED; a RUNNING request stayed running in the tracker even though the HTTP response unconditionally claimed {"status":"cancelled"}, particularly when killRender() had nothing to kill (renderer already wedged behind a modal) or the request was a plain non-render script (no kill mechanism at all). Both cancel paths now mark the request CANCELLED immediately, matching the response, while still attempting a best-effort killRenderOnMainThread() for render requests.

Explicitly out of scope (see #34 comment for full findings): suppressing the modal itself, or a headless mode. No hook exists anywhere in the public DAZ Studio SDK to intercept the Iray render-error modal (raised by the closed-source dzirayrenderer.dll), and a generic dialog-auto-click watchdog was evaluated and rejected as too risky — it could misfire on a legitimate interactive dialog a human is looking at. The fixes above bound the API-visible symptom instead: /execute fails fast with 503 immediately, and any in-flight async request (including cancels) resolves to a terminal status within a bounded window rather than hanging indefinitely.

Closes #34.

Test plan

  • ./build.sh build — clean build, no errors
  • Live-verified against running DAZ Studio 4: render that succeeds but fails to save (blocking "Error Saving" modal) no longer leaves /execute permanently busy
  • Live-verified with a temporarily-lowered stale-running timeout: a render stuck behind the modal correctly flips to failed and stays failed after the modal is later dismissed
  • Live-verified cancel-while-running: a busy-loop request (non-render, unkillable) reaches "running", is cancelled, and both the cancel response and a follow-up status poll agree on "cancelled" instead of the tracker staying stuck at "running"
  • pytest tests/test_api.py -k "cancel or async" passing against the live plugin

SceneEventBroker previously only paired enterBusy(Rendering) with
DzScene::renderFinished(DzRenderer*). When a render errors, DAZ Studio
pops a blocking modal dialog and renderFinished never fires, leaving
the plugin permanently busy and every subsequent /execute returning
503 until Studio is restarted.

DzRenderMgr::renderFinished(bool succeeded) is now connected as a
second, guaranteed exit path -- it fires even on render failure. A
m_renderBusy guard flag ensures the two "finished" signals don't
double-decrement the busy depth counter when both fire for the same
render.

Closes daz-script-server-3l5o.
Records the GH #34 investigation, fix, and live DS4/DS6 verification
notes for daz-script-server-3l5o, plus creation of the follow-on
beads (eaub, q0l4, j1pg).
…34)

AsyncRequestManager had no watchdog for a request stuck in RUNNING
forever -- if the underlying DazScript call blocks behind a DAZ
Studio modal (e.g. a render that succeeds but fails to save), GET
/requests/:id would report "running" indefinitely with no way for a
client to detect failure short of an unbounded wait.

Adds AsyncRequestManager::failStaleRunning(), invoked from the
existing 5-minute cleanup timer, which marks any request RUNNING for
longer than ServerConfig::ASYNC_STALE_RUNNING_TIMEOUT_MIN (30 min
default) as FAILED with a diagnostic message. Since the underlying
main-thread call may still be blocked and complete later once a human
dismisses the dialog, markCompleted() now no-ops if the request is
already in a terminal state, so a late real completion can't flip a
timed-out request's status back to completed/cancelled.

Live-verified on DAZ Studio 4 (temporarily lowered timeout/interval
to 1 min for a fast test, reverted before this commit): a render
stuck behind an "Error Saving" modal correctly flipped to "failed"
with the timeout message while the modal was still up, and stayed
failed after the modal was later dismissed.

Closes daz-script-server-eaub.
…is a no-op (GH #34)

cancelJson()/cancelRenderJson() only transitioned a request to CANCELLED
when it was still QUEUED. For a RUNNING request, they set cancelRequested
and fired killRenderOnMainThread() (best-effort, render-only), but left
the tracker's status as 'running' while the HTTP response unconditionally
claimed {"status":"cancelled"}. If killRender() had nothing to kill (e.g.
the renderer already wedged behind a modal) or the request was a plain
non-render script (no kill mechanism at all), GET /requests/:id would
report 'running' forever, contradicting the cancel response.

Both paths now mark the request CANCELLED immediately, the same way the
QUEUED branch already did, while still attempting killRenderOnMainThread()
as a best-effort interrupt for render requests. markCompleted() already
no-ops for requests not in RUNNING state (added for the stale-running
watchdog, daz-script-server-eaub), so a late real completion from an
underlying call that couldn't actually be killed can't flip the status
back.

Closes daz-script-server-q0l4.
#34)

Live-verified against DAZ Studio 4: 2 billion loop iterations took
60-120s to actually finish on the main thread, which was fine for the
test itself (which only waits for "running" then cancels) but starved
later tests' main-thread-dependent calls while it drained in the
background. 500 million iterations (~8s) still reliably lands in
"running" before cancel, with much less residual blocking afterward.
@ghirpara
ghirpara merged commit 5435b53 into master Aug 15, 2026
3 checks passed
@ghirpara
ghirpara deleted the fix/render-error-hang branch August 15, 2026 04:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failed renders hang the server indefinitely behind a modal error dialog

1 participant