Skip to content

An execute refused by the shutdown gate takes an ordering ticket it never releases, contradicting remote.hpp's own stated invariant #348

Description

@Yaraslaut

Summary

An execute envelope refused by RemoteServer's shutdown gate takes a per-model ordering ticket that it never releases. This violates an invariant the file states explicitly about itself.

Found while documenting the execute-ordering gate for #342.

The mechanism

handleImpl takes the ticket before posting to the pool, for any well-formed execute with a non-zero modelId (include/morph/core/remote.hpp:337-340):

if (auto peek = ::morph::wire::decode(msg); peek.kind == "execute" && peek.modelId != 0) {
    ::morph::exec::detail::ModelId const mid{peek.modelId};
    ticket.emplace(mid, takeExecuteTicket(mid));
}

It then posts, and the pool thread calls dispatchMessage(msg, reply, cid, ticket) (:349).

dispatchMessage's shutdown gate returns before reaching dispatchExecute, and therefore before any rejectAndRelease (:937-941):

if ((env.kind == "register" || env.kind == "execute" || env.kind == "attach") &&
    _shuttingDown.load(std::memory_order_acquire)) {
    reply(::morph::wire::encode(::morph::wire::makeErr("server shutting down", env.callId)));
    return;
}

execute is in that list, so the ticket taken at :337 is dropped without release.

The invariant it breaks

Stated in the file itself, at :1211-1218:

Every early-return branch below that follows the ticket-taking site must release it (via releaseExecuteTicket) before returning — an unreleased ticket permanently stalls every later ticket for the same model.

The shutdown gate is exactly such an early-return branch, and it does not release.

Why it is benign today, and why it should still be fixed

beginShutdown() is irreversible — nothing un-sets _shuttingDown (see the comment at :1698). So every later execute for that model is refused at the same gate and never reaches awaitExecuteTurn. Nothing is left waiting on the stalled ticket, and no caller hangs.

What remains is a leaked gate map entry on a shutting-down server, and — more importantly — a stated invariant that is false. The next person to add an early return after the ticket site will read :1211-1218, see it presented as universally held, and reasonably assume the shutdown path already honours it. An invariant with a live exception that the comment does not mention is a trap, independent of today's blast radius.

Verification status

Read from code on master c429195c, not reproduced. handleImpl's ticket site, dispatchMessage's gate, and the invariant comment were each read directly; the early return is plainly upstream of every rejectAndRelease in that function. No test was written to observe a leaked entry.

Suggested direction

Either release the ticket in the shutdown branch before returning, or move the gate above the ticket-taking site in handleImpl so a refused envelope never takes one. The second is arguably cleaner — a message that will be refused outright has no ordering to preserve — but it moves a shutdown check onto the transport thread, which is a deliberate placement decision (:333-341 takes the ticket there precisely so send order is captured before the pool can reorder), so it deserves a moment's thought rather than a mechanical move.

Whichever is chosen, :1211-1218's wording should end up true rather than aspirational.

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