Skip to content

fix(extensions): recover worker after failed texture setup - #259

Open
Souptik96 wants to merge 1 commit into
lightningpixel:devfrom
Souptik96:fix/extension-worker-texture-setup-recovery
Open

fix(extensions): recover worker after failed texture setup#259
Souptik96 wants to merge 1 commit into
lightningpixel:devfrom
Souptik96:fix/extension-worker-texture-setup-recovery

Conversation

@Souptik96

Copy link
Copy Markdown

Fixes #239

Root cause

A generation with enable_texture builds the texture pipeline lazily, and extensions free the shape pipeline first to make room for it. When that setup fails — a missing xatlas being the case reported in #239 — the generator is left with _model = None while the worker subprocess stays alive.

Nothing then resets the loaded state, at either layer:

  • api/runner.py called gen.generate() unconditionally in its generate branch, with no check that a model was actually in memory.
  • ExtensionProcess._loaded stayed True. It is only cleared by unload(), stop() and the cancel hard-kill path — never by a failed generation.
  • GeneratorRegistry.get_active() only calls load() if not gen.is_loaded(), so it skipped the reload.

Both layers therefore believed the worker was loaded while _model was None, and every later generation died on self._model(...) with:

TypeError: 'NoneType' object is not callable

…until the worker was killed by hand, exactly as the issue describes.

Fix

Minimal, and follows the pattern already used elsewhere in the codebase (the cancel hard-kill path drops state precisely so the model will reload on next run):

  1. api/runner.py_ensure_model_loaded(gen) runs before inference and reloads when the model is gone, mirroring what GeneratorRegistry.get_active() already does host-side. A reload emits a warning log so the cause stays visible rather than being silently papered over.
  2. api/runner.py — the error payload now carries loaded, the worker's post-failure state, read through _generator_is_loaded() which never raises (a half-initialised generator can make is_loaded() itself throw, and an unreadable state is reported as not loaded so the caller reloads).
  3. api/services/extension_process.py — on an error reporting loaded: false, _loaded is cleared so get_active() reloads before the next run instead of reusing a broken worker.

The original failure is still surfaced unchanged: the first attempt fails with the real cause (the missing xatlas), and it is the retry that now succeeds. No automatic in-run retry or backoff was added, since the failure is usually a missing dependency that will not fix itself within a run — recovery is offered on the next explicit attempt instead.

Verification

Test command is the repo's own: python -m unittest discover -s tests from api/ (what npm run test:py invokes).

api/tests/test_runner.py gains a driver that runs runner.main() end-to-end against a throwaway extension whose lazy texture setup fails on the first attempt and succeeds on the second — the exact sequence from the issue — asserting the first run errors with the real cause, the retry returns done, and _model is genuinely set again afterwards.

Negative control — with the tests in place but both source changes reverted, 8 of the new tests fail, and the retry reproduces the reported symptom verbatim:

FAIL: test_worker_recovers_after_failed_texture_setup
AssertionError: 'error' != 'done' : retry did not recover:
{'type': 'error', 'id': 'run-2', 'message': "'NoneType' object is not callable", ...}
FAIL: test_clears_loaded_when_worker_reports_model_lost
FAIL: test_reload_before_generate_is_logged
ERROR: test_failed_run_reports_that_the_model_was_lost
ERROR: (4x) GeneratorLoadedStateTests
Ran 59 tests — FAILED (failures=3, errors=5, skipped=2)

With the fix applied: Ran 59 tests — OK (skipped=2). Four of the twelve new tests are deliberate invariant controls (a failure that keeps its model must keep _loaded; an error must still propagate its traceback; a successful run must not reload) and pass in both states.

Not verified

The reporter's environment (RTX 5090 / CUDA 13.3 / real xatlas texgen) was not available to me, so the failure is reproduced through a generator that models the documented _model = None lifecycle rather than a real texture pipeline. The changed code paths are hardware-independent — protocol and loaded-state bookkeeping only. The JS/TS suites and ESLint were not run as no JS/TS files are touched.

A generation with enable_texture builds the texture pipeline lazily, and
extensions free the shape pipeline first to make room for it. When that
setup failed (missing xatlas being the common case) the generator was
left with _model = None while the worker process stayed alive, and
nothing reset the loaded state:

- runner.py called gen.generate() unconditionally, with no loaded check.
- ExtensionProcess._loaded stayed True, because it is only cleared by
  unload(), stop() and the cancel hard-kill path, not by a failed run.
- GeneratorRegistry.get_active() therefore skipped load().

Every later generation then raised "TypeError: 'NoneType' object is not
callable" until the worker was killed by hand.

The runner now ensures the model is loaded before inference, mirroring
what get_active() already does host-side, and reports its post-failure
loaded state so ExtensionProcess can drop its cached flag and reload on
the next run. The original failure is still surfaced unchanged.

Fixes lightningpixel#239
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.

1 participant