compute: a per-process registry of published index arrangements - #38387
compute: a per-process registry of published index arrangements#38387antiguru wants to merge 3 commits into
Conversation
0b83619 to
e4df1e7
Compare
e4df1e7 to
db592b9
Compare
db592b9 to
acaae62
Compare
e0c1fed to
7eeb13c
Compare
7eeb13c to
5c1ad88
Compare
5c1ad88 to
882fe25
Compare
1b830fc to
086d395
Compare
086d395 to
9687c2c
Compare
9687c2c to
16675c9
Compare
| struct Waker { | ||
| /// Fires the interactive worker out of `step_or_park`. `Send`, minted by the worker via | ||
| /// `sync_activator_for`. | ||
| activator: SyncActivator, |
There was a problem hiding this comment.
Do we want to wake a specific worker or thread? For the latter, just use a handle to the thread and call unpark.
There was a problem hiding this comment.
A specific worker, and switched to the thread as you suggest.
register_waker is called from handle_compute_command on the interactive worker's own thread (server.rs), so std::thread::current() is available at the registration site, exactly as at the peek-offload one. Waker now holds a Thread and mark calls unpark().
Two reasons beyond consistency. Every timely allocator's await_events bottoms out in std::thread::park, so the unpark is not a coincidence of the current allocator choice being the thread one. And, as the offload path's comment already notes, a root-path SyncActivator additionally marks the worker's dataflows schedulable, which is work this wake does not need: the server loop drains take_dirty on its own, it does not need an operator rescheduled.
What is lost is SyncActivationError as a "worker has gone away" signal, which the doc called a backstop. It is not a state we survive into: a panic on either runtime's worker aborts the process through the shared-fate hook, so there is no live registry with a departed worker behind it. That paragraph is gone.
This also removed the ParkedWorker harness in the tests, about 80 lines. It existed only to keep activate() on its non-erroring path, and unpark has no erroring path. The three dirty-set tests now register thread::current(), which the harness doc already said was sufficient for what they assert. sync_activator_fires_cross_thread stays, retargeted at shared_trace's ImportQueue, which is the remaining user of a real activation.
Posted by Claude Code.
| /// Marks `id` dirty for interactive worker `worker_index` and fires its waker. | ||
| /// | ||
| /// The seal signal: maintenance calls this from an export's frontier probe when the shared | ||
| /// trace's `upper` advances, so a fast-path peek waiting on the seal is re-examined. Delegates | ||
| /// to `notify`. | ||
| pub(crate) fn note_frontier(&self, id: GlobalId, worker_index: usize) { | ||
| self.notify(id, worker_index); | ||
| } |
There was a problem hiding this comment.
Inlined. note_frontier was a bare delegation to notify, and its only other content was a doc sentence naming the caller. The frontier probes in render.rs and logging/initialize.rs now call notify directly, and the seal-signal sentence moved onto notify, where the rest of the caller contract already lives.
Posted by Claude Code.
| /// taken, so it stays outside the lost-wakeup argument. | ||
| pub(crate) fn notify(&self, id: GlobalId, worker_index: usize) { | ||
| let ids = self.notify_closure(id); | ||
| let mut wakers = self.inner.wakers.lock().expect("registry poisoned"); |
There was a problem hiding this comment.
Should wakers be a read-write lock?
There was a problem hiding this comment.
No, every access to wakers mutates, so an RwLock would only ever take the write lock. notify and notify_all insert into the dirty set and set the coalescing flag, take_dirty takes the set and clears the flag, register_waker installs an entry. There is no read-only path to share.
The lock that could plausibly be an RwLock is map, which handles only reads. It stays a Mutex because get_or_create and reexport need the read and the write to be one critical section: the whole point is that whichever side touches a slot first creates it and the other observes the same Arc, which a read lock upgraded to a write lock would not give.
Posted by Claude Code.
16675c9 to
ccffa13
Compare
QA LLM Review1. HIGH --
|
ccffa13 to
0225ab1
Compare
|
Both confirmed. Fixed in 0225ab1 by removing A re-exported index now gets its own publication point. Posted by Claude Code. |
74f1b05 to
f2531cf
Compare
Publishing an arrangement produces handles, but a reader on another thread needs a way to find them. This adds a registry keyed by `GlobalId` and worker ordinal, shared across all timely workers of a process the way the persist client cache is, so worker `i` publishes into slot `i` and a reader on worker `i` of another runtime looks up the same slot. That is sound only because both sides shard keys by `key.hashed() % peers`, which the equal-peer requirement guarantees. A slot can be created before it is filled. `get_or_create_placeholder` lets whichever side touches an id first create the slot and the other adopt it in place, so a reader never overwrites a publisher's arrangement or imports over a slot that a later publish replaces. Readers learn about publication and seal through a dirty-id inbox with a coalescing `SyncActivator` rather than by polling. Nothing in the crate calls the registry yet, so it is inert in the same sense as the shared-trace primitive it builds on. `published_logical_holds` is `pub` because its callers arrive later and crate scoping would make it unreachable for dead-code analysis. Tests are out of line in `sharing/tests.rs`, per the convention in `src/compute/AGENTS.md`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`note_allow_compaction` fed the publisher the controller's compaction frontier so it could approximate the trace's `since`. The trace now publishes its own `since`, so the registry only carries the standing hold. Tests keep a trace agent alive for as long as they read, since the point closes with the trace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
f2531cf to
93cfbd9
Compare
Every critical section is a few map operations and the publisher takes the lock once per seal, so separate locks for the slots and the wakers bought nothing but an acquisition-order rule. The lost-wakeup argument on `notify` is about the publication and the mark being separate critical sections, and stands as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDm7opomJLxbNUEP3r9BLk
Second of eight PRs splitting #37770. Stacks on #38386. Tracked by CPU-215.
Publishing an arrangement produces handles, but a reader on another thread needs a way to find them. This adds a registry keyed by
GlobalIdand worker ordinal, shared across all timely workers of a process the way the persist client cache is. Workeripublishes into slotiand a reader on workeriof another runtime looks up the same slot, which is sound only because both sides shard keys bykey.hashed() % peers.A slot can be created before it is filled.
get_or_createlets whichever side touches an id first create the slot, backed by unbacked publication points, and the other adopt it in place, so a reader never overwrites a publisher's arrangement nor imports over a slot a later publish replaces.publishis the one entry point for a publisher: it adopts the slot's points and marks the id dirty. Readers learn about publication and seal through a dirty-id inbox that unparks the worker's thread rather than by polling.Inert: nothing constructs a registry.