Skip to content

core: a handler attached to a settled Completion is isolated like one attached before - #825

Open
Yaraslaut wants to merge 1 commit into
masterfrom
fix/completion-late-attach-throw
Open

Yaraslaut wants to merge 1 commit into
masterfrom
fix/completion-late-attach-throw

Conversation

@Yaraslaut

@Yaraslaut Yaraslaut commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

What failed on master

The master CI run for 64d339aa (#809) failed one job, Application ladder / ASan+UBSan (run 36115939775):

/home/runner/work/morph/morph/examples/common/testkit/test_presenter.cpp:219: FAILED:
  REQUIRE( morph::ladder::testkit::pumpUntil([&] { return !presenter.busy(); }) )
due to unexpected exception with message:
  presenter probe: onOk threw

It has nothing to do with #809. The cause is in include/morph/core/completion.hpp.

Cause

CompletionState delivers a handler through one of two paths:

  • Handler attached before settlement: setValue/setException post one composed closure. It wraps every handler in its own try/catch, logs, and continues (completion.md, "Per-handler exception isolation").
  • Handler attached after settlement: attachThen/attachOnError post the handler through their fire-now closure without that wrapping, so a throw escapes into the executor. With Qt as the executor, it surfaced through pumpUntil / processEvents.

Which path runs is a race between the producer settling and the consumer attaching. The test comment already relies on the isolation ("Completion's executor ... itself catches (and logs) a throwing one rather than letting it escape to pumpUntil's caller"), and that held only when the attach won. ASan slows the attaching thread enough for the backend to settle first some of the time.

Fix

Both fire-now closures carry the same try/catch + logError as the composed closures. docs/spec/core/completion.md now states that isolation covers both paths.

Verification

  • Two new tests in tests/test_completion_multi_handler.cpp attach a throwing then/onError handler after settlement on InlineExecutor, which makes the race deterministic. Without the fix both fail (REQUIRE_NOTHROW(comp.then(...)) → due to unexpected exception with message: handler blew up). With it, [completion] passes 78/78.
  • Full local suite (GCC 16.2.1, Qt 6.11.2): 3069/3070. The one failure, kanban.BoardView keeps a usable board area and keeps every column reachable, fails identically on master locally and is not among the tests CI's ladder jobs run, so it is unrelated to this change and to the master CI failure.
  • clang-tidy-diff.py over the diff is clean, and a planted finding on a changed header line was reported, so the header lines are being analysed. clang-format 22 is clean.

Not verified: I did not reproduce the original ASan flake locally. The evidence that this is its cause is the stack of the failure (the handler's exception escaping the event loop) and the deterministic core reproduction above, not a before/after of the flaky ladder test itself. Re-open condition: the ladder test failing the same way with this change in.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y4eif7wQNNhSkHUKYqq5Xq

… attached before

setValue/setException wrap each composed handler in try/catch, log and
continue. attachThen/attachOnError's fire-now path, taken when the handler
is attached after settlement, posted the handler bare, so a throwing handler
escaped into the executor. Which path runs is a race between the producer
settling and the consumer attaching; under ASan the backend thread settles
first often enough that ladder's "Presenter::track() calls finishOne() even
when onOk itself throws" failed on master (run 36115939775) with the throw
escaping pumpUntil.

Both fire-now closures now carry the same isolation. Two tests attach a
throwing handler after settlement on the inline executor, which makes the
race deterministic; both fail without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4eif7wQNNhSkHUKYqq5Xq
@Yaraslaut Yaraslaut added bug Something isn't working area: core Subsystem: core labels Sep 25, 2026
@codecov

codecov Bot commented Sep 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Subsystem: core bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant