refactor(spider-scheduler): Move dispatch queue ownership into the scheduler core. - #443
Conversation
…cheduler core: * Make the core own and construct its dispatch queue instead of receiving a pre-built sink as a parameter to `run`, so that a core is free to own a dispatch structure that is not a single channel. `SchedulerCore` loses its `Sink` associated type and gains `get_dispatch_queue_source`, and the runtime now builds the core first and takes the read handle from it. * Drop `Clone` from `DispatchQueueSource`'s supertraits so the trait is object-safe, and flatten `DispatchQueueReader` so the single `Arc<dyn DispatchQueueSource>` handed to the service is the only indirection. * Delete the `DispatchQueueSink` trait, whose sole implementor became a concrete type once the core owned its writer, moving `enqueue`, `bump_session_id` and `size` to an inherent `impl DispatchQueueWriter`. * Drop the now-unused `SchedulerConfig::dispatch_queue_capacity`, and the `DispatchQueueSourceType` type parameter from `SchedulerServiceState` and `GrpcSchedulerService`.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review. WalkthroughThe scheduler now owns its dispatch queue. The API uses ChangesDispatch queue ownership refactor
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to This change moves dispatch-queue ownership into the scheduler core while preserving scheduling behavior and service access through a read handle; no actionable merge-blocking risk remains beyond normal checks and review. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…heduler core: * Make the core own and construct its dispatch queue instead of receiving a pre-built sink as a parameter to `run`, so that a core is free to own a dispatch structure that is not a single channel. `SchedulerCore` loses its `Sink` associated type and gains `get_dispatch_queue_handle`, and the runtime now builds the core first and takes the handle from it. * Delete the `DispatchQueueSink` trait, whose sole implementor became a concrete type once the core owned its writer, moving `enqueue`, `bump_session_id` and `size` to an inherent `impl DispatchQueueWriter`. * Rename `DispatchQueueSource` to `DispatchQueueHandle`, since with the sink gone it named one half of a pair that no longer exists, and add a `SharedDispatchQueueHandle` alias for its `Arc`. Drop `Clone` from the trait's supertraits so it is object-safe, and flatten `DispatchQueueReader` so the handle carries a single level of indirection. * Drop the now-unused `SchedulerConfig::dispatch_queue_capacity`, and the `DispatchQueueSourceType` type parameter from `SchedulerServiceState` and `GrpcSchedulerService`.
7657eb9 to
61b17e5
Compare
| /// # Returns | ||
| /// | ||
| /// The current size of the dispatch queue. |
There was a problem hiding this comment.
Do we really need this docstring?
There was a problem hiding this comment.
Removed. I thought the docstring is necessary since we now make the API public. But looks like clippy doesn't complain if I remove it.
|
Verified that e2e tests from #385 pass after merge. |
Description
The scheduler runtime currently creates the dispatch queue, splits it into a
(writer, reader)pair, hands the writer to the core as a parameter ofSchedulerCore::run, and keeps the reader for the execution-manager-facing service. That arrangement assumes the dispatch queue is one channel, and two consequences follow from the assumption:create_runtimecallsmake_core::<SchedulerStorageClientType, DispatchQueueWriter>(), so every core implementation is forced to share one dispatch queue type.This PR inverts the ownership: the core constructs and owns whatever dispatch structure its algorithm needs, and exposes only a read handle to the service. The runtime builds the core first, takes the handle from it, and then builds the service.
API changes
SchedulerCore::Sink(associated type)SchedulerCore::run(self, storage_client, sink, ..)SchedulerCore::run(self, storage_client, ..)SchedulerCore::get_dispatch_queue_handle(&self) -> SharedDispatchQueueHandletrait DispatchQueueSource: Send + Sync + Clonetrait DispatchQueueHandle: Send + Syncpub type SharedDispatchQueueHandle = Arc<dyn DispatchQueueHandle>trait DispatchQueueSinkenqueue,bump_session_idandsizebecome inherent methods onDispatchQueueWriterSchedulerServiceState<DispatchQueueSourceType>SchedulerServiceStateGrpcSchedulerService<DispatchQueueSourceType>GrpcSchedulerServiceSchedulerConfig::make_core<SchedulerStorageClientType, DispatchQueueSinkType>()SchedulerConfig::make_core<SchedulerStorageClientType>()SchedulerConfig::dispatch_queue_capacity()create_runtime(..) -> (Runtime, SchedulerServiceState<DispatchQueueReader>, CancellationToken)create_runtime(..) -> (Runtime, SchedulerServiceState, CancellationToken)Behaviour is unchanged. The queue is still created at
SessionId::default()with the capacity from the core's own config, which is exactly what the runtime did before.Why
Arcand notBoxCloneimpliesSized, so the trait could not be made into a trait object whileClonewas a supertrait. Dropping it is what makesdyn DispatchQueueHandlepossible;#[async_trait]boxes the returned future, sodequeueremains callable through the trait object.The handle is an
Arcwith exactly one level of indirection.DispatchQueueReaderpreviously held anArc<DispatchQueueReaderInner>, so returningArc<dyn DispatchQueueHandle>unchanged would have wrapped anArcin anArc— three pointer hops fromSchedulerServiceStateto the channel where there had been two.DispatchQueueReaderInneris therefore flattened intoDispatchQueueReader, whose two remaining fields (session_idandassignment_receiver) are already cheap-clone handles.Boxwas considered and rejected: it removes the double indirection, but hands the service a source with no guarantee of cheap cloning. Nothing clones it today, but that is a property of the current service rather than of the contract, and the contract is what a second core inherits.Why
DispatchQueueSinkis deleted rather than keptBefore this change the trait was load-bearing:
RoundRobinCorewas generic overDispatchQueueSinkType: DispatchQueueSink, so the bound did real work. Once the core owns a concreteDispatchQueueWriter, the only holder is that concrete type, every call resolves statically, and the trait's sole remaining effect is forcing an import to bring the methods into scope.The surviving trait stays a trait, and the asymmetry is the point: it is the seam the service depends on, and a second core will implement it differently.
DispatchQueueSinkhad no second implementor coming.With
DispatchQueueSinkgone,DispatchQueueSourcenamed one half of a pair that no longer existed, so it is renamed toDispatchQueueHandle— the sole external access point to a core's dispatch queue, which is expected to grow further caller-to-core methods beyonddequeue. ItsArcalias follows theShared<Thing>convention already used byspider-storage'sSharedRwandSharedJobControlBlock.One consequence worth flagging for review:
sizegained#[must_use]. As a trait method it was exempt fromclippy::must_use_candidate; as a public inherent method it is not, and the pedantic lint group is deny-level in this workspace.What deliberately does not change
These were considered and left alone, so please read them as decisions rather than as misses:
DispatchQueueHandle::dequeuekeeps its signature. A resource-group-aware core needs to distinguish a pinned execution manager from a general one, whichdequeue(wait_time)cannot express. The minimal shape isdequeue(&self, resource_group_id: Option<ResourceGroupId>, wait_time: Duration), with the round-robin implementation ignoring the argument. Deferred to a follow-up PR to keep this diff reviewable.SchedulerCore: Sendis retained, andcreate_runtimestill spawnscore.run(..)withtokio::spawn. A core whose scheduling state is notSendcannot be spawned that way; addressing it is a separate change and does not affect anything here.RoundRobinis that it holds a concreteDispatchQueueWriterfield instead of a generic sink, renamed todispatch_queue_writerto match.create_dispatch_queueremains public. The round-robin core calls it frommake_core, and the white-box tests call it directly.Notes for reviewers
The round-robin tests already passed the real
DispatchQueueWriterrather than a mock, so the test changes are mechanical: the helpers stop threading asinkparameter and take the core's reader for assertions instead.RoundRobin::rundestructures*selfwith.., which drops the core's dispatch queue reader. That is safe becauseDispatchQueueWriterInnerholds its own clone of theasync_channel::Receiver, so the channel does not close even ifget_dispatch_queue_handleis never called.Checklist
breaking change.
Validation performed
Summary by CodeRabbit