| #include "NVMeDevice.hpp" |
| |
| #include <boost/system/detail/error_code.hpp> |
| #include <phosphor-logging/lg2.hpp> |
| |
| #include <cassert> |
| |
| void NVMeDevice::start() |
| { |
| if (intf.getProtocol() == NVMeIntf::Protocol::NVMeMI) |
| { |
| setup(); |
| } |
| } |
| |
| void NVMeDevice::setup() |
| { |
| dev->setup( |
| [weak{weak_from_this()}](const std::error_code& ec, |
| const std::shared_ptr<MctpEndpoint>& ep) { |
| if (auto self = weak.lock()) |
| { |
| self->finalize(ec, ep); |
| } |
| }); |
| } |
| |
| void NVMeDevice::stop() |
| { |
| timer->cancel(); |
| } |
| |
| void NVMeDevice::finalize(const std::error_code& ec, |
| const std::shared_ptr<MctpEndpoint>& ep) |
| { |
| assert(intf.getProtocol() == NVMeIntf::Protocol::NVMeMI); |
| |
| if (ec) |
| { |
| restart(); |
| return; |
| } |
| |
| ep->subscribe( |
| [weak{weak_from_this()}](const std::shared_ptr<MctpEndpoint>& ep) { |
| if (auto self = weak.lock()) |
| { |
| self->degraded(ep); |
| } |
| }, |
| [weak{weak_from_this()}](const std::shared_ptr<MctpEndpoint>& ep) { |
| if (auto self = weak.lock()) |
| { |
| self->available(ep); |
| } |
| }, |
| // Removed |
| [weak{weak_from_this()}](const std::shared_ptr<MctpEndpoint>& ep) { |
| if (auto self = weak.lock()) |
| { |
| self->removed(ep); |
| } |
| }); |
| } |
| |
| void NVMeDevice::restart() |
| { |
| assert(intf.getProtocol() == NVMeIntf::Protocol::NVMeMI); |
| assert(timer); |
| assert(gracePeriod); |
| // Setup failed, wait a bit and try again |
| timer->expires_after(*gracePeriod); |
| timer->async_wait( |
| [weak{weak_from_this()}](const boost::system::error_code& ec) { |
| if (ec) |
| { |
| return; |
| } |
| |
| if (auto self = weak.lock()) |
| { |
| self->setup(); |
| } |
| }); |
| } |
| |
| void NVMeDevice::degraded(const std::shared_ptr<MctpEndpoint>& ep) |
| { |
| assert(intf.getProtocol() == NVMeIntf::Protocol::NVMeMI); |
| lg2::info("[{ENDPOINT}]: Degraded", "ENDPOINT", ep->describe()); |
| recovering = true; |
| gracePeriodActive = false; |
| timer->cancel(); |
| std::get<std::shared_ptr<NVMeMiIntf>>(intf.getInferface())->stop(); |
| } |
| |
| void NVMeDevice::available(const std::shared_ptr<MctpEndpoint>& ep) |
| { |
| assert(intf.getProtocol() == NVMeIntf::Protocol::NVMeMI); |
| assert(timer); |
| assert(gracePeriod); |
| lg2::info("[{SUBSYS}][{ENDPOINT}]: Available", "SUBSYS", name, "ENDPOINT", |
| ep->describe()); |
| |
| // The behaviour of MctpEndpoint::subscribe() is to register the provided |
| // callbacks, and then fetch the current state of the endpoint. As a |
| // consequence of fetching the state, the appropriate callback is be |
| // invoked. |
| // |
| // Unless circumstances are particularly rough, the endpoint will be in the |
| // 'Available' state when it is first exposed on DBus. |
| // |
| // Beyond the start-up entry to the 'Available' state, we will only see an |
| // 'Available' event if something has triggered endpoint recovery (putting |
| // the endpoint into 'Degraded'), followed by the recovery succeeding. |
| // |
| // However, it's possible that recovery is triggered by the NVMe MI layer |
| // being broken, while the MCTP endpoint continues to respond without error. |
| // This can lead to a "tight" event cycle of entering 'Degraded', |
| // immediately followed by entering 'Available', where NVMeMi::start() kicks |
| // off the endpoint recovery again as the MI layer continues to fail. |
| // |
| // The impact of this event cycle is exacerbated by `nvme_mi_open_mctp()` |
| // performing IO to the device for the purpose of quirk detection. Currently |
| // `nvme_mi_open_mctp()` is issued on the main thread, and if the MI |
| // layer is dead enough, the device fails to respond to the Admin Identify |
| // command. The failure to respond blocks the main thread for the default |
| // libnvme-mi command timeout (which, upstream, is 5000ms). This can lead |
| // to misbehaviour of applications interacting with the DBus objects exposed |
| // by nvmesensor. |
| // |
| // Mitigate the hazard of this event cycle by enforcing a grace-period after |
| // MCTP endpoint recovery succeeds. This can provide some time for other |
| // jobs on the main thread to be processed. |
| // |
| // We track whether the grace period is required via the `recovering` |
| // boolean. If we've passed through the 'Degraded' state then we know we're |
| // at risk of the tight event cycle, therefore we enforce the grace-period. |
| // If we've not passed through 'Degraded' but we have an 'Available' event |
| // we know we're in the start-up path, which we don't want to delay. |
| // |
| // State machine for events and grace period: |
| // ```mermaid |
| // stateDiagram-v2 |
| // [*] --> Setup : Device Initialized |
| // |
| // state "Setup (Initial State)" as Setup |
| // state "Active (Interface Started)" as Active |
| // state "Degraded State\n(recovering = true)" as Degraded |
| // state "Grace Period Active\n(Timer Ticking)" as GracePeriod |
| // |
| // %% Setup transitions |
| // Setup --> Active : Available Event\n(Bypasses Grace Period) |
| // Setup --> Degraded : Degraded Event |
| // |
| // %% Active transitions |
| // Active --> Degraded : Degraded Event\n(Stops Interface) |
| // Active --> Setup : Removed Event\n(Stops Interface, Restarts) |
| // |
| // %% Degraded transitions |
| // Degraded --> GracePeriod : Available Event\n(Sets |
| // recovering=false,\nStarts Timer) Degraded --> Setup : Removed |
| // Event\n(Stops Interface, Restarts) |
| // |
| // %% Grace Period transitions |
| // GracePeriod --> Active : Timer Expires\n(Starts Interface) |
| // GracePeriod --> GracePeriod : Available Event\n(Ignored, Timer |
| // Continues) GracePeriod --> Degraded : Degraded Event\n(Cancels Timer, |
| // Stops Interface) GracePeriod --> Setup : Removed Event\n(Cancels |
| // Timer, Restarts) |
| // ``` |
| if (recovering) |
| { |
| recovering = false; |
| gracePeriodActive = true; |
| timer->expires_after(*gracePeriod); |
| timer->async_wait( |
| [weak{weak_from_this()}, ep](const boost::system::error_code& ec) { |
| if (auto self = weak.lock()) |
| { |
| if (ec) |
| { |
| self->gracePeriodActive = false; |
| lg2::info( |
| "[{ENDPOINT}] grace period expired due to: {ERROR}", |
| "ENDPOINT", ep->describe(), "ERROR", ec.message()); |
| return; |
| } |
| // Ensure we haven't degraded or been removed while the |
| // callback was waiting in the ASIO event queue. |
| if (!self->gracePeriodActive) |
| { |
| return; |
| } |
| self->gracePeriodActive = false; |
| std::get<std::shared_ptr<NVMeMiIntf>>(self->intf.getInferface()) |
| ->start(ep); |
| } |
| }); |
| } |
| else |
| { |
| // If the grace period is actively running, this is a consecutive |
| // Available event. We ignore it to avoid bypassing the grace period. |
| if (gracePeriodActive) |
| { |
| return; |
| } |
| |
| // If we're not recovering we should only see an 'Available' event |
| // when we're first initialising the device. There should be no timers |
| // to cancel. |
| std::size_t cancelled = timer->cancel(); |
| if (cancelled != 0) |
| { |
| lg2::error( |
| "[{ENDPOINT}]: Unexpected timer cancellation ({CANCELLED}) in non-recovery path!", |
| "ENDPOINT", ep->describe(), "CANCELLED", cancelled); |
| } |
| std::get<std::shared_ptr<NVMeMiIntf>>(intf.getInferface())->start(ep); |
| } |
| } |
| |
| void NVMeDevice::removed(const std::shared_ptr<MctpEndpoint>& ep) |
| { |
| assert(intf.getProtocol() == NVMeIntf::Protocol::NVMeMI); |
| lg2::info("[{ENDPOINT}]: Removed", "ENDPOINT", ep->describe()); |
| recovering = false; |
| gracePeriodActive = false; |
| std::get<std::shared_ptr<NVMeMiIntf>>(intf.getInferface())->stop(); |
| restart(); |
| } |