feat: host-side scheduled wakeup primitive for delegates (#3972)#4666
Draft
sanity wants to merge 1 commit into
Draft
feat: host-side scheduled wakeup primitive for delegates (#3972)#4666sanity wants to merge 1 commit into
sanity wants to merge 1 commit into
Conversation
Implement the host side of the delegate scheduled-wakeup primitive whose wire variants land in freenet-stdlib 0.8.3. - WakeupScheduler (contract/wakeup.rs): a per-node schedule with a time-ordered forward index, a (delegate, tag) reverse index for cancel-by-tag, and ReDb persistence so pending wakeups survive restart. Each wakeup stores the delegate's params so a fired wakeup replays in the delegate's own param context (the DelegateKey is a one-way hash of code+params, so params can't be recovered from the key -- correcting the empty-params shortcut the contract- notification path takes). - OutboundDelegateMsg::ScheduleWakeup is intercepted in handle_delegate_with_contract_requests and persisted; it is fire-and-forget and does not re-drive the delegate. - Wakeups fire INLINE as a new arm in the contract_handling select! loop -- the same task that owns the executor and runs delegates -- so there is no cross- task channel, no lossy dispatch, and a newly-scheduled earlier wakeup is observed on the loop's next pass with no explicit notification. Firing delivers InboundDelegateMsg::WakeupFired; an uninstalled delegate yields a missing-delegate error that is logged and dropped (the entry was already removed by take_due), never reinstalled. Requires freenet-stdlib 0.8.3 (published separately; stdlib-first). Testing: WakeupScheduler unit tests (cancel-by-tag replace, fire ordering, ReDb persistence across reload, bounded batch, volume/no-loss); a new test-delegate-wakeup WASM delegate + runtime tests exercising ScheduleWakeup emission and WakeupFired delivery across the WASM boundary, and the drop-on-uninstalled path. Closes #3972 [AI-assisted - Claude] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8mqiskQracG7CDVLSxDJC
Collaborator
Author
|
Depends on freenet/freenet-stdlib#82 (adds [AI-assisted - Claude] |
This was referenced Jul 2, 2026
Collaborator
Author
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.
Problem
Delegates cannot schedule future execution, so always-on background work (River
private-room key rotation #228, TTL pruning, scheduled publish) has to live in a
UI sync loop and stops when the UI is closed. This implements the host side of
the scheduled-wakeup primitive whose wire variants land in freenet-stdlib 0.8.3.
Approach
WakeupScheduler(crates/core/src/contract/wakeup.rs) — a per-nodeschedule with:
BTreeMap<(SystemTime, seq)>) so "what firesnext" is O(1),
(delegate, tag)reverse index for cancel-by-tag (re-scheduling thesame tag replaces the prior pending wakeup), and
WAKEUPS_TABLE) so pending wakeups survive restart;the table is created on first open of an upgraded DB.
Params capture. A fired wakeup must invoke the delegate with the same params
it was registered with, but the
DelegateKeyis a one-way hash of code+params sothe host can't recover them from the key. Each wakeup therefore stores the
delegate's params at schedule time and replays them on fire — correcting the
empty-params shortcut the contract-notification path takes.
Inline firing (no separate task).
OutboundDelegateMsg::ScheduleWakeupisintercepted in
handle_delegate_with_contract_requestsand persisted(fire-and-forget, not re-driven). Wakeups fire inline as a new arm in the
contract_handlingselect!loop — the same single task that owns the executorand runs delegates. That means no cross-task channel, no lossy dispatch, and no
Notify: because scheduling and firing are on the same task, a newly-scheduledearlier wakeup is observed on the loop's next pass. Firing delivers
InboundDelegateMsg::WakeupFired; a delegate that is no longer registered yieldsa missing-delegate error that is logged and dropped (the entry was already
removed by
take_due), never reinstalled — the issue's "drop on uninstalleddelegate" rule.
This deviates from the issue's suggested standalone-tokio-task sketch on purpose:
firing on the delegate-owning task avoids the whole class of cross-task
channel/back-pressure bugs the project's channel-safety rules exist to prevent.
Testing
WakeupSchedulerunit tests: cancel-by-tag replace, earliest-first fireordering, bounded batch, ReDb persistence across reload (simulated restart),
cancel-by-tag persisted, and volume/no-loss for many independent schedules.
test-delegate-wakeupWASM delegate + runtime tests: a delegate emitsScheduleWakeup(host sees it), a host-deliveredWakeupFiredreaches thedelegate's
process()and it reacts — exercising both new variants across theWASM bincode boundary — plus the drop-on-uninstalled path.
Closes #3972
[AI-assisted - Claude]
🤖 Generated with Claude Code
Review
Full-tier multi-model review run pre-PR (wire format + delegate runtime +
concurrency + persistence): Codex (external) + 4 diverse Claude reviewers
(concurrency, persistence/wire-format, adversarial bug-hunt, big-picture). The
data-race and lost-wakeup questions were traced and confirmed safe (single-task
&mut, no cross-task mutation). Findings addressed in this PR:ScheduleWakeuparm inprocess_outboundno longer drain-and-breaks; it falls through so eachtrailing
SendDelegateMessagereaches its own attesting arm (also preservesContextUpdatedfolding).decode_valuepanic on a corrupt row (persistence/bug-hunt): now validatesnanos and uses
checked_add, returningNoneso a torn row is skipped, not astartup crash. New overflow test.
global, and tag-length caps enforced at
schedule(); new cap tests.TimeSourcedeviation now carries a load-bearing justificationcomment; at-most-once delivery documented on the stdlib variant; the shared
"migration probes" warn generalised for the uninstalled-wakeup case.
Known follow-ups (filed separately): a loop-level integration test for the
firing glue (a deterministic one is blocked by the intentional wall-clock design;
tracked as an issue), and the pre-existing drain-bypass in the sibling terminal
arms (
ApplicationMessage/RequestUserInput/… — not introduced here).