Repository navigation
Actor as a pull-driven generator: one-turn protocol, close(), scheduler without global new - #185
Open
kotbegemot wants to merge 3 commits into
Open
kotbegemot wants to merge 3 commits into
kotbegemot wants to merge 3 commits into
Conversation
…er without global new The actor's loop is a coroutine generator in a frame from the actor's resource; resume() pulls one step of it, like next(). The turn -- the right to call resume() -- lives in the mailbox: a blocked mailbox holds it, the send() that unblocks it takes it (needs_sched). A suspended behavior keeps the turn (verdict `resume`); the actor parks only once its frame is suspended. Results never wake anyone. Protocol and lifecycle - detail/actor_protocol.hpp: a participant count plus `destroying`, no CAS loops; the actor_state word, resume_impl and their helpers are gone. - Contract violations stop the process: resume() without a turn, after close() or from inside a behavior; a second delete; a delete that cannot drain (bounded wait); in debug, two resume() calls at once and an actor awaiting its own send(). - close() returns unique_future<void>: a marker behind everything sent so far; idempotent; once ready, delete is safe while the scheduler runs. - delete is a destroying operator delete; actor classes must be final. A suspended chain unwinds before ~Actor and its callers get broken_pipe. - A message lives as long as its behavior; dispatch() is [[nodiscard]] and passes a method's error code (co_return ec) on to the caller instead of aborting. Memory and scheduler - sharing_scheduler(resource, threads, max_throughput): a ring of job_ptr values and the workers come from the resource; send -> enqueue -> resume allocates nothing globally. - Any T&& method parameter is a compile error. A promise nobody took a future from frees its state; dropping a unique_future cancels a suspended producer; try_complete_finalize no longer leaks when the future is released between the CAS and the fallback. Removed: ref_counted, intrusive_ptr, make_counted, unique_future::actor_promise, dead behavior_t members (is_awaited_ready() merged into take_awaited_continuation()), mailbox push_front/size/empty, message::prev, test/contended-resume, test/intrusive_ptr, tests_stress_intrusive_ptr, test/race-condition/TESTING.md. Tests: protocol-violations, actor-teardown, close, message-lifetime, dispatch-nodiscard, dispatch-error-code, behavior-member-only, scheduler-no-new, rvalue-param-prohibited, park-window one_step; a Relacy model of the turn handover, close() and delete (test/relacy, ALLOW_RELACY, its own CI workflow); detect_leaks=1 in the sanitizer CI. Compiler workarounds: GCC 13 ICEs on assigning a co_yield result in a template; GCC 11 lost the value of a temporary awaited with a class-type await result. Docs: docs/LIFECYCLE.md (the turn, verdicts, close(), contract checks), CLAUDE.md, PROMISE_FUTURE_GUIDE.md, CHANGELOG.md with migration guides. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- behavior_t::promise_type::operator new: GCC 11.4 deduces the implicit object argument's type as Actor&, so has_resource_method<const Self> formed a pointer to a reference. Strip the reference before the check. - scheduler_t keeps its workers in a plain array from its resource instead of a std::pmr::vector: libstdc++ 11 destroys a vector's elements through the deprecated polymorphic_allocator::destroy, which Clang 18 reports and -Werror fails. A destructor now frees the workers start() made. Checked in ubuntu:22.04 with the CI's flags (WARNINGS_AS_ERRORS=ON, benchmarks, examples): g++-11 11.4 and clang++-18, Debug, EXCEPTIONS_DISABLE=ON: builds clean, ctest 170/170. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The mailbox keeps push_back, pop_front, try_block and close, with their old signatures; nothing replaces what goes. - try_unblock() is gone: it handed out a turn without a message. - blocked() is gone: pop_front() on a parked mailbox now stops the process with the "resume() on a parked actor" violation. In release it used to unpark the mailbox silently, handing the turn to nobody; only the actor's entry check stood in the way. - closed() is gone: close() of a parked actor also frees the loop's frame (parked, it holds no behavior and no message), so resume() after close() stops at the entry check (!loop_ || loop_.done()), and the check after a failed park became unreachable. - lifo_inbox.hpp is untouched; the Relacy model mirrors the new calls. Red first: test/mailbox-api pins try_unblock()'s absence (try_compile); actor-teardown sees the loop frame freed at close(); protocol_violation_release_pop_parked_mailbox carried on before. Green: local Debug; Relacy 5/5 with the four mutants; ubuntu:22.04 with the CI's flags (WARNINGS_AS_ERRORS, benchmarks, examples): g++-11 11.4 and clang++-18, Debug, 172/172. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
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.
Summary
The actor becomes a pull-driven generator: its message loop is a coroutine in a frame from the actor's memory resource, and
resume()pulls one step of it, likenext(). The turn — the right to callresume()— lives in the mailbox; thesend()that unblocks a parked mailbox takes it (needs_sched). A suspended behavior keeps the turn (verdictresume), and the actor parks only once its frame is suspended. Results never wake anyone.docs/LIFECYCLE.mddescribes the model end to end.Protocol and lifecycle
detail/actor_protocol.hpp: one participant counter plusdestroying; no CAS loops. Theactor_stateword,resume_impland their helpers are gone.resume()without a turn, afterclose(), or from inside a behavior; a seconddelete; adeletethat cannot drain (bounded wait);resume()calls at once, and an actor awaiting its ownsend().close()→unique_future<void>: a marker queued behind everything sent so far; idempotent. Once its future is ready, destroying the actor is safe while the scheduler runs.deleteis a destroyingoperator delete; actor classes must befinal. A suspended chain unwinds before~Actor, and its callers getbroken_pipe.dispatch()is[[nodiscard]]and passes a method's error code (co_return ec;) on to the caller instead of aborting.Memory and scheduler
sharing_scheduler(resource, threads, max_throughput): the job queue (a ring ofjob_ptrvalues) and the workers come fromresource. Once warmed up,send → enqueue → resumedoes no global allocation.T&&method parameter is a compile error.promisenobody took a future from frees its state;unique_futurecancels a suspended producer;try_complete_finalize()no longer leaks.Removed
ref_counted,intrusive_ptr,make_counted;unique_future::actor_promise;behavior_tmembers (is_awaited_ready()is merged intotake_awaited_continuation());push_front,size,emptyandmessage::prev;test/contended-resume,test/intrusive_ptr,tests_stress_intrusive_ptr,test/race-condition/TESTING.md.Breaking changes
Migration guides are in
CHANGELOG.md.std::pmr::memory_resource*first.final.T&&method parameters.resume(), or one without a turn, is a contract violation. Before, it returnedawaiting.Test plan
ctest(clang, macOS): 170/170.dispatch()version: Release 171, ASan 170, TSan 169 +rt_tuplerun alone, UBSan 170, exceptions ON 170.ctest170/170 each, 0 warnings. GCC 12 and Clang 14 (libstdc++ 12): smoke build and run.gcc:13): 5/5, all four mutants caught.leaks --atExit: 0 across every test binary andbalancer.Deferred, not in this PR:
close()orstop()first;push_back/pop_front/park/close.🤖 Generated with Claude Code