Skip to content

core: SynchronousBackendAdapter::cancelPending settles the completion but the queued control call still registers on the wrapped backend #636

Description

@Yaraslaut

Summary

Found while fixing #619, and deliberately not folded into it: #619 is about
the completion the adapter produced never being settled by cancelPending;
this is about the work behind that completion still happening after the
cancellation. Fixing one does not fix the other, and they need different
mechanisms.

SynchronousBackendAdapter::dispatch (include/morph/core/backend.hpp) posts a
task to its private _control strand:

_control.post(kControlStrand, [shared, op = std::move(op)]() mutable {
    try {
        shared->resolve(op());
    } catch (...) {
        shared->reject(std::current_exception());
    }
});

After #619, cancelPending rejects shared. It does nothing to op. A task
that has not started yet still runs op() when it reaches the head of the
strand, which for a bind is inner->bindModelBlocking(request) — an actual
registerModelWithContext/registerModelShared/attachModel on the wrapped
backend. The resolve that follows is a no-op on an already-rejected state, so
the caller is told the bind was cancelled while the registration goes through
anyway.

Why it matters

cancelPending's two callers are ~Bridge (bridge.hpp) and
Bridge::switchBackend. In the ~Bridge case the instance is created on a
backend whose Bridge is gone, so nothing will ever deregisterModel it: it
is a leaked live instance for as long as the backend lives. In the
switchBackend case it is created on the outgoing backend after the swap.

Severity today is latent for the same reason #619's is: grep -rn SynchronousBackendAdapter finds no production call site on this revision, only
tests/test_backend_registration_surface.cpp and prose. morph#571, which
proposes making this surface the default path, is what would make it live.

Verification status

Reproduced, as a side observation of #619's own regression test, on the
laneCORE-batch-615-619-620 branch (base a8511aa6), GCC 16.2.1, Debug,
Linux. The test wraps a backend whose registerModelWithContext blocks until
released, cancels while it is blocked, then releases it. Real output from that
case, which passes:

All tests passed (92 assertions in 11 test cases)

The assertions that record this behaviour are, after the cancellation has
already been delivered as an error:

    inner->letGo();
    REQUIRE(morph::testing::waitUntil([&] {
        callerExec.runOnce();
        return inner->finished.load() == 1;
    }));

inner->finished is incremented by the wrapped backend's control call on its
way out, so == 1 is the observation: the registration completed after
cancelPending returned. The test asserts it because that is current
behaviour; it is the assertion that would have to change if this issue is
fixed.

What I did not verify: that any real backend leaks anything as a result. No
production call site exists to leak through, so the leak is inferred from
reading ~Bridge/switchBackend (neither deregisters an instance it never
learned the id of), not measured.

Sketch of a fix (not implemented)

Give each dispatched task a shared_ptr<std::atomic_bool> cancellation flag
alongside its promise, set every live one in cancelPending, and have the task
return without calling op() when it is set. That closes the "queued but not
started" window, which is all that can be closed: a call already inside
op() cannot be recalled, and SynchronousBackendAdapter has no way to
interrupt a blocking verb it does not implement.

What would change the verdict

  • Close it if a maintainer decides the adapter is documented as best-effort
    here — that a cancelled bind may still register — in which case
    IBackend::cancelPending's contract wants that carve-out written down, since
    the wrapped backend's own cancelPending does not have it.
  • Raise severity if morph#571 lands and this adapter becomes a production
    path, or if any caller is found that relies on cancelPending meaning "no
    further registration will reach the backend".

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreSubsystem: corebugSomething isn't workingtriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions