Skip to content

Consider: deadline/cancellation propagation across the executor abstraction #116

Description

@Yaraslaut

Part of #115 (userver-vs-morph survey).

Observation

morph has a per-call executeTimeout/setExecuteDeadline (LimitPolicy), but nothing threads one deadline through an entire call tree the way userver's engine::Deadline + server::request::TaskInheritedData does (deadline propagates through the task tree and across the wire — X-YaTaxi-Client-TimeoutMs for HTTP, native grpc-timeout for gRPC — so a client-side timeout can abort work several async hops downstream). userver also has cooperative cancellation tokens (engine::current_task::CancellationPoint()/ShouldCancel(), TaskCancellationBlocker); morph's IExecutor has no cancellation-token concept at all.

Status: speculative, not a confirmed gap

This is not a current pain point. For a single Model::execute call, morph's existing per-call timeout is probably sufficient — there's no fan-out today for a deadline to usefully propagate across.

Revisit only if

morph's actions start fanning out into multiple downstream calls per request (e.g. an action that itself triggers further model mutations or downstream service calls), at which point a single top-level deadline not reaching the nested calls becomes a real problem.

Reference

Full comparison: #115, §3.1 and §4 of the attached document (docs/superpowers/findings/userver-vs-morph-2026-08-17.md).


Deep analysis: #116 deadline/cancellation propagation

Verification against current source. The trigger condition ("actions fan out into multiple downstream calls") remains unmet — and the prior comment's own most-likely candidate has now resolved against it: kanban is implemented on PR #121 (board_model.cpp, project_admin_model.cpp) and both models are synchronous, single-transaction execute() bodies with zero framework dispatch inside them, exactly like all 17 implemented models in bank/bookmarks/pastebin/polls. crm/forge/ledger/lims are still README-only. The single-hop primitives are unchanged: Bridge::setExecuteDeadline (include/morph/core/bridge.hpp:895, armed per-call at bridge.hpp:1150-1161) and LimitPolicy::executeTimeout (include/morph/core/remote.hpp:44, 1169-1172), both on TimeoutScheduler.

The minimal design is the right shape, with two refinements. Threading a deadline through session::Context (include/morph/session/session.hpp:26) is correct: Context already crosses the wire (core/wire.hpp:122), so cross-process propagation comes free — the analogue of userver's X-YaTaxi-Client-TimeoutMs. Refinements: (a) propagate remaining milliseconds, restamped per hop — absolute steady_clock points don't transfer across machines; (b) note it is prototypable today via Context::metadata["deadline-ms"] with zero schema change, which is itself evidence no framework machinery is needed yet. The alternatives are worse: a cancellation token on IExecutor contradicts a recorded spec decision (docs/spec/core/executor.md:354, 376-382 — fire-and-forget, "No cancellation") and would burden all three executors, the strand layer, and the browser-timer WASM build (timeout_scheduler.hpp:57); std::execution alignment is likewise already explicitly declined (executor.md:363). Server-side cooperative cancellation would break a documented invariant — "morph never interrupts an in-flight Model::execute" (docs/spec/core/completion.md:276) — so propagation could only ever bound waits, not work.

Interaction with #127/#128. This is the strongest reason not to build now. A deadline/cancel path is a third concurrent settler racing the strand dispatch and the timeout (see remote.hpp:1326 and the check-then-use analysis at bridge.hpp:1135-1150) — precisely the completion-chain/teardown territory where #127 (fixed on the kanban branch) and #128 (open, 165 TSan warnings) live. Adding settle paths before #128 is understood would multiply that surface, not tame it.

Cost/benefit. Effort M (Context field, restamp logic in executeVia and RemoteServer dispatch, spec + Doxygen updates, tests across local/remote/threaded/WASM). Risk moderate-plus given the open teardown races. Beneficiaries today: none — no call site has a second hop.

Recommendation: defer, keep open. Concrete trigger: the first model or bridge-level orchestration that makes a second, separately dispatched executeVia/wire call whose wait is not bounded by the first hop's own deadline. Kanban did not become that; nothing else is close. Resolve #128 first regardless.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions