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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions