Runloop: extracts parts and atomize state access#236
Conversation
self use4107853 to
7c80534
Compare
e9e47a3 to
e8b0022
Compare
Coverage Report
Compared to |
017e486 to
f7da748
Compare
f7da748 to
323396e
Compare
piercefreeman
left a comment
There was a problem hiding this comment.
This refactor is shaping up really nicely. Leaving my initial comments below.
| let mut executor_shards = HashMap::from([(executor_id, 0usize)]); | ||
| let lock_tracker = InstanceLockTracker::new(Uuid::new_v4()); | ||
| let mut inflight_actions: HashMap<Uuid, usize> = HashMap::new(); | ||
| let mut inflight_dispatches: HashMap<Uuid, InflightActionDispatch> = HashMap::new(); | ||
| let mut sleeping_nodes: HashMap<Uuid, SleepRequest> = HashMap::new(); | ||
| let mut sleeping_by_instance: HashMap<Uuid, HashSet<Uuid>> = HashMap::new(); | ||
| let mut blocked_until: HashMap<Uuid, DateTime<Utc>> = HashMap::new(); | ||
| let mut barrier: CommitBarrier<ShardStep> = CommitBarrier::new(); | ||
| let mut instances_done_pending: Vec<InstanceDone> = Vec::new(); | ||
| let (sleep_tx, _sleep_rx) = tokio::sync::mpsc::unbounded_channel::<SleepWake>(); | ||
|
|
||
| let step = ShardStep { | ||
| executor_id, | ||
| actions: vec![], | ||
| sleep_requests: vec![SleepRequest { | ||
| node_id, | ||
| wake_at: requested_wake_at, | ||
| }], | ||
| updates: None, | ||
| instance_done: None, | ||
| }; | ||
|
|
||
| let mut worker_pool = MockWorkerPool::new(); | ||
| worker_pool.expect_queue().never(); | ||
|
|
||
| let before = Utc::now(); | ||
| let params = super::Params { | ||
| executor_shards: &mut executor_shards, | ||
| lock_tracker: &lock_tracker, | ||
| inflight_actions: &mut inflight_actions, | ||
| inflight_dispatches: &mut inflight_dispatches, | ||
| sleeping_nodes: &mut sleeping_nodes, | ||
| sleeping_by_instance: &mut sleeping_by_instance, | ||
| blocked_until_by_instance: &mut blocked_until, | ||
| commit_barrier: &mut barrier, | ||
| instances_done_pending: &mut instances_done_pending, | ||
| sleep_tx: &sleep_tx, | ||
| worker_pool: &worker_pool, | ||
| skip_sleep: true, | ||
| step, | ||
| }; |
There was a problem hiding this comment.
Seems like we're sharing the same basic constructors across all these test functions - a helper function that returns the default params (or a test-scoped extension to Params that allows for construction without these values) might make this code cleaner.
There was a problem hiding this comment.
6283c11 (this PR) how is this? If this looks good enough I'll distribute the same approach to the rest of the tests
There was a problem hiding this comment.
I agree the harness approach is cleaner (at least for these cases where we have to initialize a pretty large payload of params that are redundant across test functions).
I'm unclear on the separation of concerns between just directly mutating the harness properties via a harness.executor_shards = xyz (which you do to override the dicts) and the arguments that are intended to go in the params() signature. Would it make sense to just have "default" values for all of the parameters and go with harness assignment for them all? Or are we only expecting to house empty-collections within the harness and everything else is intended to be a params() param?
There was a problem hiding this comment.
The separation of concerns is arbitrary; I think it is a mistake. Let me iterate a bit more on this to we can simplify; we can reduce the harness to just a bag of values really.
We probably don't want to generalize and reuse it among the tests for different parts - as it would needlessly broaden the scope of the tests. So each part would get a test harness that only accepts overrides via fields, and a direct params conversion.
There was a problem hiding this comment.
On the separation of concerns - there is a bit of a practical thing though - the params instance holds some non-Copy fields by value by design, in this case step; so, those would still have to be passed explicitly via params argument. Also the mocks just have shorter access paths if they're provided from the outside - but there's actually no practical need to ever construct them separately - so I'm inlining them as well for now; if there's a need we can move them back to .params args.
Goes in after #236 This PR continues the runloop refactors, now focusing on removing more logic from the `impl Runloop { ... }` blocks. We are introducing purposely-built primitive with a strict API for the tracking of the available instance slots.
This PR refactors the main runloop body into smaller parts, and atomizes access to the variables by effectively hand-rolling the explicit closures as params.
This change helps by moving the runloop implementation from a waterfall of code to a more structured sequence of fn calls with explicit parameter captures, thus enabling high-level reasoning and overview.
The code (for now) is organized into two major pieces:
partsandops.partsare what's going to hold the chunks of runloop, as in they were, pretty much following the linear logic and structure of the extracted code;opsare the bits that were corresponding to the reusable operations previously residing atimpl Runloop { ... }, and accessible viaselfThe access to both runloop local variables and
selfvariables is now explicitly provided via params. This can a bit verbose, but this verbosity also provides an intuitive understanding as to how complex a block of code really is, and an explicit overview of what data it can access. We will rely on this later when we will be analyzing usage patterns and encapsulation designs for data used by the runloop and shards.Further refactors are to be be added on top of this.
Among other things, notable things to do next are:
RunLooptype altogether, and move to running the loop via a standalone fn;loop, therunloopdoes a lot of extra setup work,spawns other loops and thread, and generally does things that are more fitting for the setup code than for the actual runloop; these parts should be separated into their own composable setup routines, and sometimes into whole separate subsystems / loops; this work should be accompanied by additional unit-testing of the newly separated and testable components;Uuid) to newtypes (likeLockId,InstanceId,ExecutionId,NodeId, etc) for added type-safety; together with this, give the variables more contextually-specific names - likelock_uuidshould bethis_execution_lock_id;Why switch from passing vars via
selfto viaParams?To do:
partsandopsNote this PR adds a respectable 1.5% test coverage; in actuality there are new tests that are catching certain things that have already been covered by python tests, however that was more accidental than intentional; we explicitly add tests (at the unit level) to ensure the small logic bits we now have work as intednded.