nvme_driver: no longer drops create io queue request when inspect is called + test#3405
nvme_driver: no longer drops create io queue request when inspect is called + test#3405gurasinghMS wants to merge 11 commits intomicrosoft:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses an NVMe driver servicing bug where TaskControl cancellation (triggered by inspect) could abandon in-progress work (notably create_io_queue), and adds a VMM integration test intended to validate the corrected behavior during servicing/save/restore.
Changes:
- Adjust
DriverWorkerTask’s run loop to only be cancellable while awaiting the next command, avoiding mid-request cancellation. - Add a new OpenHCL servicing VMM test that issues inspect calls during a delayed IO-queue-creation fault and then performs save/restore validation.
- Add the
inspectcrate as a dependency forvmm_tests.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
vm/devices/storage/disk_nvme/nvme_driver/src/driver.rs |
Moves the until_stopped cancellation boundary to only cover recv.next() in the worker loop. |
vmm_tests/vmm_tests/tests/tests/multiarch/openhcl_servicing.rs |
Adds a new integration test exercising inspect during delayed IO queue creation + subsequent save/restore assertions. |
vmm_tests/vmm_tests/Cargo.toml |
Adds inspect dependency required by the new test’s inspect-node matching. |
Cargo.lock |
Records the new inspect dependency in the lockfile. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
I think this is strictly better behavior, but I'm a little concerned about not being able to inspect while something is wedged. |
I agree with ya! I was thinking about this and to keep inspect availabillity high we could save the future and when |
|
Do you mind elaborating further on why inspect causes this task to be canceled/stopped? |
Added to the PR description. |
|
Adding a doc comment to TaskControl in #3414 |
|
Thanks for this! I think you identified a similar problem in the ioq as well. Mind taking that fix here as well? |
Previously the run loop for the
DriverWorkerTaskwould abandon work in progress when TaskControl cancelled the task to service an inspect call. This would cause thecreate_nvme_io_queuespan to exit before the queues were properly set up. With this change the task should only be cancelled when waiting onnext(). However, if active work is being done this won't abandon current work in order to service an inspect call.There are downsides to this approach, however. If a device is being really slow to process a create command, we won't be able to service saves AND (with this change) won't be able to inspect system state either.
Added a vmm_test as well that verifies such behavior by calling inspect on nvme driver with a delayed CREATE_IO_COMPLETION_QUEUE and then verifies that save was not serviced before create completion.
Why inspect causes this task to be canceled/stopped?
Looking at the implementation of
TaskControl/StopTask/StopTaskInner, we see thatTaskControl, once started, invokesrun()on the underlying task type (in our caseDriverWorkerTask) and passes in two parameters:task.insert())StopTaskFocusing on
StopTask::until_stopped(), we see that it accepts a future that can be stopped usingTaskControl::stop(). This function works by polling two futures:until_stopped, which in our case is an async loop (or whatever future the user wants to run)StopTaskInnerZooming in further to
StopTaskInner::poll, we can see that it can returnPoll::Ready()for two reasons:!shared.calls.is_empty()← This becomes importantshared.stop == trueThe shared calls are important because they allow
TaskControlto update existing task state while the task is running. This is done by updating theVec<>of function calls and invoking the waker associated with theStopTask, which is polling both the user function andStopTaskInner. This is, in my understanding, the same waker that the user functionfwould invoke when more work is available.When this happens,
StopTaskInner::pollreturnsPoll::Ready(Err(Cancelled)), at which point the entire task exits and yields execution back to the internal components ofTaskControl.TaskControlthen executes all the calls accumulated inshared.callsand invokesrun()on the underlying task again, provided the task was not stopped by the user.The side effect of all of this is that the future the user was awaiting inside
until_stoppedgets discarded. Crucially, this is also the same pathway leveraged byInspectandInspectMutto inspect the underlying task. As a result, when thenvme_driveris inspected, it causes the task to be cancelled and re-run. In our case, this would discard thecreate_nvme_io_queuefuture being awaited, putting the system into a bad state.A simple fix here is to only wrap the
recv.next()inuntil_stopped, which is also the pattern used elsewhere in the codebase. The downside here is that we can't stop stuck create_nvme_io_queue or save commands anymore ... but we were not doing that previously either. If either of those are stuck, we need to tear everything down anyways.The relationships between these objects can be mapped out as:
