blob: 78015309543eaacb99e40d10c60af861c0717011 [file]
#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();
}