Skip to content

Flower ServerApp templates should relay their own exceptions to the hub - #1007

Open
atriaybagur wants to merge 2 commits into
developfrom
1006-flower-serverapp-exception-relay
Open

Flower ServerApp templates should relay their own exceptions to the hub#1007
atriaybagur wants to merge 2 commits into
developfrom
1006-flower-serverapp-exception-relay

Conversation

@atriaybagur

@atriaybagur atriaybagur commented Aug 19, 2026

Copy link
Copy Markdown
Member

Closes #1006. The in-run half of #1001 (whose external half — the hub's failed-job poll — is #1003).

What changes

Both Flower templates (fl-apps/flower/{standard,evaluation}/app/server_app.py) get a relay shell around their run body:

  • main() becomes a thin wrapper: try: _run(...) except Exception: _relay_failure(...); raise.
  • _relay_failure sends the full traceback.format_exc() to the hub via flip.send_handled_exception (a success=false activity-feed row; the ServerApp runs on the Central Hub, so no trust boundary is crossed — unlike ClientApp exceptions, where the recorded design decision is type+message only) and settles the model to ERROR. Each step is guarded separately, so an unreachable hub or a non-UUID model id (tutorial/simulator contexts, where FLIP() resolves to the dev no-op anyway) cannot mask the original failure.
  • The exception is re-raised, so Flower still records finished:failed and the fix: surface FL runs that fail after submission (#1001) #1003 sweep stays consistent — its post-poll model-status re-read makes the double sighting a no-op.
  • The researcher-supplied models.py import moves from module scope into the guarded body: a broken models.py — the most likely researcher error — now lands its traceback on the activity feed instead of killing the ServerApp before it can say anything (the [Bug]: a Flower run that fails after submission is invisible — model sits at INITIATED with no error #1001 canonical case, previously visible only through the sweep's captured log tail).

Existing in-body error paths are unchanged: parse_best_model_run_config failures now additionally get their traceback relayed by the shell (they raise); save/upload failures still return after their own ERROR and never reach the shell. A second update_status(ERROR) for the parse path is a no-op on the hub (same-status transition).

Merge order relative to #992

Deliberately sequenced after #992 (flower-min-clients-from-trust-count): both PRs touch the same five files (the two template server_app.py and their three CI-pinned tutorial mirrors). #992 is load-bearing for more than itself — it fixes the min_nodes=2 single-trust hang that blocks Flower on a second dev instance — while this PR is self-contained hardening that rebases trivially (the relay wraps main(); #992 changes strategy construction inside the body). When #992 lands, this branch gets rebased, the five files re-resolved, and check_tutorial_sync re-run.

Why the helper is duplicated per template, not added to flip.flower

Templates ship in the flip-api image; flip-utils ships in the FL images — they deploy separately. A template importing a new flip.flower symbol dies with ImportError at module scope on any FL image older than the template: the exact failure mode this change exists to end. Self-containment costs ~20 duplicated lines and removes the skew entirely.

Scope

  • Covers every hub-deployed Flower run: bundle_flower_application skips user files whose names collide with template files, so the template's server_app.py always wins.
  • NVFLARE needs nothingflip_server_event_handler already relays terminal failures (FATAL_SYSTEM_ERROR, END_RUN).
  • Tutorial mirrors (fl-tutorials/flower/*/app/server_app.py) are resynced byte-identically — scripts/check_tutorial_sync.sh pins them as exact copies of the templates (CI enforces it; the first push here went red on exactly that). In the tutorials' standalone make submit path FLIP() resolves to the dev no-op implementation, so the relay just logs there — behaviourally inert outside a hub deployment.
  • Not covered (unchanged): module-scope failures in the template's own imports (torch, flip, flwr — operator-environment, not researcher, errors) and ClientApp deaths at trusts. The fix: surface FL runs that fail after submission (#1001) #1003 sweep remains the backstop for both.

Verified

  • Behavioural check of both templates via a scratch pytest (flip-utils venv, flwr 1.32.1, torch stubbed; not committed — fl-apps/ has no test harness, per the issue):
    • both server_app modules import with no app/models.py present — proving the module-scope decoupling (pre-change this import was the module-scope death);
    • an exception raised from _run is relayed (send_handled_exception called once with the traceback, client_name=None, the run-config model id; update_status(<id>, ERROR)) and re-raised;
    • a relay failure (send_handled_exception raising ValueError on the non-UUID tutorial id) still settles status and re-raises the original. 6/6 passed.
  • ruff check (0.14.7, the template pyprojects' own config) clean on both app/ trees.
  • Templates keep their Apache headers; no required_files.json change (no files added/removed).
  • bash scripts/check_tutorial_sync.sh — all copies in sync after the resync commit.

NOT verified

  • No live run. No SuperLink has executed the modified templates; the scratch test calls main directly with a mocked flip and a stubbed context. The standard template is exercised end-to-end by make e2e_smoke FL_BACKEND=flower when a stack is available.

Acceptance Criteria

Imported from issue #1006

  • A hub-deployed Flower run whose ServerApp raises inside main() — in either template — moves its model to ERROR and writes the traceback to fl_logs via send_handled_exception, without waiting for the reconcile sweep.
  • A broken user models.py (ImportError/SyntaxError on get_model import) is reported the same way rather than dying at ServerApp module scope.
  • The exception is re-raised after reporting, so Flower still records finished:failed and the fix: surface FL runs that fail after submission (#1001) #1003 sweep stays consistent.
  • Error paths that already report do not produce duplicate ERROR transitions with different meanings.
  • A run with a non-UUID flip-model-id does not crash the handler — the relay degrades gracefully.
  • Both templates stay behaviourally identical for successful runs.

…#1006)

Both Flower templates' main() bodies now run inside a relay shell: any
exception escaping the run is reported to the hub — the full traceback via
send_handled_exception (server-side, no trust boundary crossed) and the model
settled to ERROR — before being re-raised so Flower still records
finished:failed and the #1003 failed-job sweep stays consistent as the
backstop. Each relay step is guarded separately so an unreachable hub or a
non-UUID tutorial model id cannot mask the original failure.

The researcher-supplied models.py import moves from module scope into the
guarded body: a broken models.py — the most likely researcher error — now
reports its traceback to the activity feed instead of killing the ServerApp
before it can say anything (the FLIP#1001 canonical case, previously visible
only to the hub's log-tail poll).

The relay helper is deliberately duplicated per template rather than added to
flip.flower: templates ship in the flip-api image while flip-utils ships in
the FL images, so a new flip.flower symbol would die with ImportError at
module scope on any FL image older than the template — the exact failure mode
this change exists to end.

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
@atriaybagur atriaybagur linked an issue Aug 19, 2026 that may be closed by this pull request
6 tasks
@github-actions github-actions Bot changed the title feat: relay Flower ServerApp exceptions to the hub from the templates (#1006) Flower ServerApp templates should relay their own exceptions to the hub Aug 19, 2026
…ates

check_tutorial_sync.sh pins these as byte-identical copies of the fl-apps
templates (flwr build excludes symlinks from the FAB, so each tutorial keeps a
real copy). The relay-shell change therefore lands in all three copies:
xray_classification and 3d_spleen_segmentation from flower/standard,
3d_spleen_segmentation_evaluation from flower/evaluation. In the tutorials'
standalone submit path FLIP() resolves to the dev no-op implementation, so the
relay just logs there — behaviourally inert outside a hub deployment.

Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
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.

Flower ServerApp templates should relay their own exceptions to the hub

1 participant