core: add a background-task seam a model's own execute() can reach (draft) - #154
Closed
Yaraslaut wants to merge 1 commit into
Closed
core: add a background-task seam a model's own execute() can reach (draft)#154Yaraslaut wants to merge 1 commit into
Yaraslaut wants to merge 1 commit into
Conversation
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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — library feature, per your standing rule. Refs #129.
Every "background job" in the ladder lives at the App/Bridge/
RemoteServerlayer 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 aRemoteServer— had nothing to reach for.ledger'sSubmitReportinvented 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.hppsetBackgroundExecutor/backgroundExecutorjournal::setActionLog's pattern for the same reason.postBackground(task, propagation)ScopedBackgroundExecutorManualExecutorNo executor installed returns
falseand 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
NoneA 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.Inheritexists 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::ScopedContextholds a reference to theContextit installs, and the caller's context is long gone by the time the task runs. Mutation-verified — capturing a pointer instead:ManualExecutorcloses the other half of the gapDeterministicExecutorandStepExecutorcontrol 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.