Read this before reviewing any change in this repository. It is the review
brief that actor-broker-handoff.md refers to as the review focus, and
it is read by Claude Code reviews as review-only instructions.
Correctness findings in the concurrency core (lib/rocoto_actor/broker.rb,
lib/rocoto_actor/reference.rb) outrank everything else. Style is enforced by
RuboCop and is out of scope. Please cite the invariant number below when a
finding shows one can be violated.
Status: the invariants were traced by the author on 2026-09-22 (see "Targeted
concurrency read" in actor-broker-handoff.md); an independent trace is
still wanted.
This section directs the concurrency review. It describes the invariants the concurrency core is supposed to uphold and the specific interleavings that have not yet had an independent review. Findings that show one of these invariants can be violated are the most valuable output.
- Per actor (
lib/rocoto_actor/reference.rb): a reader thread (read_replies), a writer thread (write_requests), and a reaper thread (start_reaper,actor_exited). All share@pending_mutex. - Per broker: a scheduler thread (
DeadlineScheduler#run: route and boot expirations, timer firing, restart backoff), an event thread (EventDispatcher#start:on_eventand watcher delivery), and up tomax_lifecycle_workerslifecycle threads (LifecycleExecutor#run_worker: actor-initiated spawn/stop and relaunches). All three are created byActorBroker#initializeand all share the broker@mutexwhen they call back into it. - Application threads call
spawn,ask,tell,stop,stop_actor, and the query methods onActorBroker. - Callbacks:
Future#on_resolveblocks run on whichever thread resolves the future (a reader thread, the service thread on expiration, or an application thread onvalue(timeout:)).Reference#on_exitblocks run on the reaper thread.Reference#send_broker_response(on_done:)callbacks run on the writer thread, or on whichever thread discards the response.
Intended rule: the broker @mutex is never held while calling into a
Reference method that takes @pending_mutex, and no Reference method calls
back into the broker while holding @pending_mutex. Please look for
violations, in particular through callbacks: on_resolve, on_exit, on_done,
attach_broker, and broker.dispatch (called from the reader thread in
read_replies).
- Every accepted broker request (
dispatch) reaches exactly one caller-visible outcome: onesend_broker_response, and itsrelease_responsecallback runs exactly once, on every path including errors, expiration, source death, and broker stop. @routesand@responses_by_sourcereturn to zero; no path leaks a slot.- A node's boot future is settled exactly once, by exactly one of
spawn(synchronous path),spawn_child(on_resolve), or the expiration, andsettleis idempotent undernode.booting. actor_exited(node, reference)ignores exits from a reference that is no longer the node's current one, and an exit during a boot is not lost (node.boot_exit).stop/stop_actorracingrelaunch: a new process can never be installed under a node that is:stopping,:stopped, or:failed, and can never be left running unowned. Seerelaunch(installed) andstop_subtrees(reference snapshot under the mutex).retirenullsnode.reference; every read ofnode.referenceoutside the mutex must have captured it under the mutex first.- Restart accounting (
restart_delay) cannot restart forever whenrestart: :on_failureand the actor never runs forrestart_window. broker.stopis idempotent, rejects new work, and joins every broker thread without deadlock, including while lifecycle jobs and relaunch tasks are queued or running.- Nothing an actor process can send over its socket (malformed frames, unknown ops, wrong types, huge frames, floods) can raise on a broker thread, block another actor's reader, or leave broker accounting inconsistent. The reader thread of the offending actor may die (that actor is then treated as failed).
- The mirror of 9, for faults on our side: nothing may leave an actor
unreachable while the broker reports it healthy. Each thread serving a
connection (
read_replies,write_requests,start_reaper) must, on any exception it did not anticipate, reject the pending futures and stop the actor rather than end quietly. A thread that dies with work queued behind it leaves the node:runningandalive?withlast_failureandlast_exitnil, and every later message to that actor times out forever — the hardest kind of failure to diagnose, because nothing anywhere records it. Callbacks that run inside those loops (on_donein the writer,on_exitin the reaper) are covered by the same rule.
- Reader thread rejects the boot future (EOF) while the reaper thread runs
actor_exitedfor the same reference; thensettleruns on the application thread. stop_subtreesmarks a:restartingnode:stoppingbetweenrelaunch'sLauncher.launchand itsinstalledcheck.- Broker
stopsets@stoppedwhile a lifecycle worker is insideLauncher.launch, and while the service thread is between selecting due tasks and running them. - Source actor dies with routes pending:
discard_control_outboxruns theon_donecallbacks from the writer thread'sensureand fromactor_exited; confirm each callback runs once. Future#value(timeout:)reaching its expiration on an application thread concurrently with the reader fulfilling the same future.- The reaper's
@reader.join(1)inReference#actor_exitedwhen the reader is itself the thread that calledkill. - An exception other than
IOError/SystemCallErrorraised insidewrite_requests— fromTransport.write_payload, or from anon_donecallback — with an ask already pending and more messages queued behind it.
Style, naming, and documentation. Scale beyond a handful of actors (terminal nodes are retained by design). Idempotency and message ids (application design).