Skip to content

core: add a background-task seam a model's own execute() can reach (draft) - #154

Closed
Yaraslaut wants to merge 1 commit into
masterfrom
model-background-tasks
Closed

core: add a background-task seam a model's own execute() can reach (draft)#154
Yaraslaut wants to merge 1 commit into
masterfrom
model-background-tasks

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Draft — library feature, per your standing rule. Refs #129.

Every "background job" in the ladder lives at the App/Bridge/RemoteServer layer and re-enters the model as a fresh, fully authorised client dispatch. A plain model instance with no App around it — the ordinary case for a keyed model on a RemoteServer — had nothing to reach for. ledger's SubmitReport invented a private executor member, and every future model needing submit-now/compute-later work would reinvent it slightly differently.

What's here — morph/core/background.hpp

Symbol Purpose
setBackgroundExecutor / backgroundExecutor Process-wide install/read, mirroring journal::setActionLog's pattern for the same reason.
postBackground(task, propagation) Post off the caller's strand.
ScopedBackgroundExecutor RAII install/restore.
ManualExecutor The worker-side test double.

No executor installed returns false and logs, rather than throwing or silently dropping. A model posting background work in a deployment that never installed one has a configuration error; it must not look like the work was scheduled.

The substantive decision: session propagation defaults to None

A background task is not the caller's request — it's work the model decided to do. Inheriting the caller's authority by default would mean a long-running job keeps acting as a principal after that principal's request finished, and (since the task outlives the dispatch) potentially after their session should have ended. A task that needs authority mints its own service-principal token, the way bookmarks' metadata fetcher already does.

Inherit exists for work that genuinely is the caller's request continued on another thread, and runs the task under a copy of the posting session.

The copy is load-bearing, not incidental. session::ScopedContext holds a reference to the Context it installs, and the caller's context is long gone by the time the task runs. Mutation-verified — capturing a pointer instead:

ERROR: AddressSanitizer: stack-use-after-scope

ManualExecutor closes the other half of the gap

DeterministicExecutor and StepExecutor control the delivery order of continuations; nothing substituted for the worker side, so every async-job test spins a real thread pool and polls against a wall-clock deadline. A submit-then-poll job is now a sequence of exact states — submit, assert "pending", runOne(), assert "complete" — with no sleeps and no flakiness budget. runAll() also picks up tasks posted by a task.

Verification

Full suite 20,168 assertions / 1,084 cases. Spec section added to docs/spec/core/executor.md.

Not done (yours)

Migrating ledger::LedgerModel's private executor member onto this seam, and deciding whether an App should install its pool automatically. I left both out because they change existing behaviour and the migration is the part worth your judgment — the primitive is deliberately additive so far.

Every "background job" in the ladder lives at the App/Bridge/RemoteServer
layer and re-enters the model as a fresh, fully authorised client dispatch.
A plain model instance with no App around it -- the ordinary case for a keyed
model on a RemoteServer -- had nothing to reach for, so ledger's
SubmitReport invented a private executor member, and every future model
needing submit-now/compute-later work would reinvent it slightly
differently.

Adds morph/core/background.hpp:

- setBackgroundExecutor/backgroundExecutor, mirroring
  journal::setActionLog's install pattern for the same reason: a model has to
  reach it without the App layer handing it one.
- postBackground(task, propagation). No executor installed returns false and
  logs, rather than throwing or silently dropping -- a model posting
  background work in a deployment that never installed one has a
  configuration error, and it must not look like the work was scheduled.
- ScopedBackgroundExecutor for tests and for an App that owns its pool.
- ManualExecutor, the worker-side test double the ladder lacked.

Session propagation defaults to None, and that is the substantive decision
here. A background task is not the caller's request; it is work the model
decided to do. Inheriting the caller's authority by default would mean a
long-running job keeps acting as a principal after that principal's request
finished, and -- since the task outlives the dispatch -- potentially after
their session should have ended. A task needing authority mints its own
service-principal token, as bookmarks' metadata fetcher already does.

Inherit runs the task under a *copy* of the posting session. The copy is
load-bearing, not incidental: session::ScopedContext holds a reference to the
Context it installs, and the caller's context is long gone by the time the
task runs. Mutation-verified -- capturing a pointer instead reproduces as
stack-use-after-scope under AddressSanitizer.

ManualExecutor closes the other half of the gap: DeterministicExecutor and
StepExecutor control the delivery order of continuations, but nothing
substituted for the worker side, so every async-job test spins a real thread
pool and polls against a wall-clock deadline. A submit-then-poll job is now a
sequence of exact states with no sleeps and no flakiness budget.

Full suite 20,168 assertions / 1,084 cases.

Refs #129

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.87234% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
include/morph/core/background.hpp 97.87% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@Yaraslaut

Copy link
Copy Markdown
Member Author

Closed with #129. Models should not know about the framework — a model implements actions and is single-threaded per ModelId; scheduling background work belongs to the App layer, and a model updates its own state later by having an action dispatched at it, which re-enters on its strand. See #129 for the full reasoning and for the ledger _reportExecutor follow-up this implies.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant