Skip to content

No framework seam for a model's own execute() to post background work and later update its own state #129

Description

@Yaraslaut

Summary

No framework-level seam exists for a model's own execute() to post work to a background executor and later update its own persisted state. Every existing "background job" pattern in the ladder examples (bookmarks' metadata-fetch worker) lives at the App/Bridge/RemoteServer layer and re-enters the model as a fresh, ordinary, fully-authorized client dispatch (through a minted service-principal token) — never something a plain model instance with no App/Bridge/RemoteServer around it can do.

Discovered while implementing rung 5 (ledger)'s SubmitReport/GetReportStatus submit→poll job (design spec called for "the job runs off the model's own strand ... following the ladder-wide 'background jobs' pattern from LADDER.md's ... internal-client-with-service-principal seam that rung 2 establishes" — this claim turned out not to be accurate; see below).

Evidence

  • morph::exec::IExecutor/ThreadPoolExecutor (include/morph/core/executor.hpp) has zero usages anywhere inside a model's own execute() across examples/bank, examples/bookmarks, examples/pastebin, examples/polls. Every real usage sits at the App/server layer (examples/bank/include/bank/app/app.hpp, examples/bookmarks/include/bookmarks/app/app.hpp, examples/bank/src/cli/main.cpp).
  • There is no process-wide singleton executor analogous to morph::journal::setActionLog's own install pattern — ThreadPoolExecutor has no set/get global accessor.
  • Bookmarks' real background job (examples/bookmarks/src/app/app.cpp, App::fetchMetadataOnce()) reads rows needing metadata via a raw SqlStatement, then for each dispatches a new client call through morph::bridge::BridgeHandler<BookmarkModel> — which itself posts through the worker pool and re-enters BookmarkModel::execute(const RecordMetadata&) as an ordinary, fully-authorized dispatch with a minted auth::kMetadataFetcherPrincipal service-principal token. BookmarkModel::execute(const RecordMetadata&) itself is synchronous, ordinary model code — it never touches an executor.
  • App::fetchMetadataOnce() is driven by a QTimer, i.e. triggered from the Qt event loop outside the model entirely — not something a headless/non-Qt model consumer (or a model with no App wrapper at all, like ledger::LedgerModel) could reach.

Impact

Any future rung/model that needs a genuine "submit now, compute later, poll for status" job (report generation, batch export, long-running aggregation) has no sanctioned framework mechanism to reach for, and must invent its own per-model workaround — exactly what ledger::LedgerModel did (a private std::shared_ptr<morph::exec::IExecutor> member, posting directly from execute(SubmitReport)). This works, but:

  • it duplicates a pattern every future model would reinvent slightly differently,
  • it has no defined service-principal/session-propagation story for the worker thread (the ledger implementation avoided this by never touching the calling thread's session — the worker re-derives everything it needs from plain copied values — but a job that genuinely needs to act as a principal on the worker thread has no guidance),
  • it cannot be tested with a deferred/deterministic executor double the way client-callback executors can (morph::ladder::testkit::DeterministicExecutor/StepExecutor exist for controlling delivery order of continuations, but no test anywhere substitutes a double for the worker side of an async job — every real async-job test genuinely spins a real thread pool and polls with a bounded, wall-clock-deadline retry loop).

Suggested direction

A first-class "background task from inside a model" primitive, with:

  • a defined way for a model to reach a shared (or model-owned) executor without inventing its own member each time,
  • a documented service-principal/session-propagation convention for code that runs on the posted task and needs to act with authority (mint a token, or explicitly document "the worker re-derives its own state and never inherits session context"),
  • a deferred/synchronous test double for the worker side specifically, so async-job unit tests don't need to spin real threads with real sleeps to get coverage.

Reference

Filed from LASTRADA-Software/morph, branch ladder-ledger-rung5 (not yet merged), as docs/findings/003-no-model-level-background-job-seam.md. See that file for the same summary in the ladder's own findings-pipeline format (examples/FINDINGS.md).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreSubsystem: coreenhancementNew feature or requesttriage: wrong-layerBelongs to a different layer, or the architecture forbids it

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions