[orc-rt] Redesign Session to provide a clearer lifecycle. (#187496)

A Session begins in the Start state. In this state no calls will be
received from the controller (since none is attached yet). This provides
clients with an opportunity to configure the Session before attaching a
ControllerAccess object with the `attach` method.

The first call to the `attach` method will register a ControllerAccess
object with the Session, and the ControllerAccess's connect method will
be called to establish a connection with the controller.

If ControllerAccess::connect fails it must call
ControllerAccess::notifyDisconnected, at which point the Session will
proceed to the Detached state.

If ControllerAccess::connect succeeds (i.e. returns without calling
notifyDisconnected) then the Session moves to the Attached state, and
calls can be made in both directions between the executor and
controller.

If at any point Session::detach is called, or if the ControllerAccess
object calls notifyDisconnected, then the Session will release its
reference to the ControllerAccess object, and all Services will have
their onDetach callbacks run.

In the Detached state no calls can be made between the controller and
executor, but existing JIT'd code may continue running. Attempts to call
the controller will result in an error.

When the shutdown method is called the Session will begin shutting down.
The Session will first be detached (if it has not been already), and
then all Services will have their onShutdown methods called.

The `addService`, `attach`, `detach`, `shutdown`, and `callController`
methods can be called from any thread. Any Service added is guaranteed
to have its onDetach and onShutdown callbacks run in order, either when
those events occur or immediately upon being added (if added after the
respective events).
This commit is contained in:
Lang Hames 2026-03-20 13:40:30 +11:00 committed by GitHub
parent 2e88fe7021
commit 19ced5ad82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 452 additions and 125 deletions

View File

@ -47,7 +47,8 @@ inline Session *unwrap(orc_rt_SessionRef S) noexcept {
class Session {
public:
using ErrorReporterFn = move_only_function<void(Error)>;
using OnShutdownCompleteFn = move_only_function<void()>;
using OnDetachFn = move_only_function<void()>;
using OnShutdownFn = move_only_function<void()>;
using HandlerTag = void *;
using OnCallHandlerCompleteFn =
@ -64,25 +65,50 @@ public:
using HandlerTag = Session::HandlerTag;
using OnCallHandlerCompleteFn = Session::OnCallHandlerCompleteFn;
ControllerAccess(Session &S) : S(&S) {}
ControllerAccess(Session &S) : S(S) {}
/// Called by the Session to disconnect the session with the Controller.
/// Initiate connection with controller.
///
/// disconnect implementations must support concurrent entry on multiple
/// threads, and all calls must block until the disconnect operation is
/// complete.
/// This will be called by the Session once it is ready to accept requests
/// from the controller.
///
/// Once disconnect completes, implementations should make no further
/// calls to the Session, and should ignore any calls from the session
/// (implementations are free to ignore any calls from the Session after
/// disconnect is called).
/// ControllerAccess implementations must not call handleWrapperCall prior
/// to connect being called.
///
/// Note: The Session may call into the controller (via callController)
/// during connect, but only in response to a controller-initiated wrapper
/// call. Callers of Session::attach must not race attach with calls to
/// Session::callController.
///
/// If connect fails to establish communication with the controller,
/// ControllerAccess implementations must call notifyDisconnected before
/// returning from connect.
virtual void connect() = 0;
/// Initiate disconnection from the controller.
///
/// The Session will call this method at most once to request disconnection
/// from the controller. However, disconnection may also be initiated by
/// the controller itself (e.g. a network socket dropping out), potentially
/// concurrently with a Session-initiated disconnect call.
///
/// ControllerAccess implementations are responsible for handling such
/// double-sided disconnection gracefully, and must ensure that
/// notifyDisconnected is called exactly once regardless of how
/// disconnection occurs. In particular, if the ControllerAccess detects
/// controller-initiated disconnection and calls notifyDisconnected, it
/// must tolerate a subsequent or concurrent call to disconnect (which
/// should be treated as a no-op).
///
/// notifyDisconnected may be called from within disconnect or
/// asynchronously after disconnect returns. This allows disconnect itself
/// to be a cheap operation (e.g. signaling a shutdown flag) with the
/// actual disconnection and notifyDisconnected call happening on another
/// thread.
virtual void disconnect() = 0;
/// Report an error to the session.
void reportError(Error Err) {
assert(S && "Already disconnected");
S->reportError(std::move(Err));
}
void reportError(Error Err) { S.reportError(std::move(Err)); }
/// Call the handler in the controller associated with the given tag.
virtual void callController(OnCallHandlerCompleteFn OnComplete,
@ -93,28 +119,37 @@ public:
virtual void sendWrapperResult(uint64_t CallId,
WrapperFunctionBuffer ResultBytes) = 0;
/// Notify the Session that the controller has disconnected.
///
/// ControllerAccess implementations must call this method exactly once
/// when the controller disconnects, whether initiated by a call to
/// disconnect, by the controller, or by a communication failure.
///
/// It is the ControllerAccess implementation's responsibility to ensure
/// exactly-once semantics for this method, even when disconnect is called
/// concurrently with controller-initiated disconnection.
///
/// No calls should be made to reportError or handleWrapperCall after this
/// method is called.
void notifyDisconnected() { S.handleDisconnect(); }
/// Ask the Session to run the given wrapper function.
///
/// Subclasses must not call this method after disconnect returns.
/// Subclasses must not call this method after notifyDisconnected is called.
void handleWrapperCall(uint64_t CallId, orc_rt_WrapperFunction Fn,
WrapperFunctionBuffer ArgBytes) {
assert(S && "Already disconnected");
S->handleWrapperCall(CallId, Fn, std::move(ArgBytes));
S.handleWrapperCall(CallId, Fn, std::move(ArgBytes));
}
private:
void doDisconnect() {
disconnect();
S = nullptr;
}
Session *S;
Session &S;
};
/// Create a session object. The ReportError function will be called to
/// report errors generated while serving JIT'd code, e.g. if a memory
/// management request cannot be fulfilled. (Error's within the JIT'd
/// management request cannot be fulfilled. (Errors within the JIT'd
/// program are not generally visible to ORC-RT, but can optionally be
/// reported by calling orc_rc_Session_reportError function.
/// reported by calling the orc_rt_Session_reportError function.)
///
/// Note that entry into the reporter is not synchronized: it may be
/// called from multiple threads concurrently.
@ -139,22 +174,12 @@ public:
/// Report an error via the ErrorReporter function.
void reportError(Error Err) { ReportError(std::move(Err)); }
/// Initiate session shutdown.
///
/// Runs shutdown on registered resources in reverse order.
void shutdown(OnShutdownCompleteFn OnComplete);
/// Initiate session shutdown and block until complete.
void waitForShutdown();
/// Add a Service to the session.
template <typename ServiceT>
ServiceT &addService(std::unique_ptr<ServiceT> Srv) {
assert(Srv && "addService called with null value");
ServiceT &Ref = *Srv;
std::scoped_lock<std::mutex> Lock(M);
assert(!SI && "addService called after shutdown");
Services.push_back(std::move(Srv));
appendService(std::move(Srv));
return Ref;
}
@ -165,21 +190,52 @@ public:
return addService(std::make_unique<ServiceT>(std::forward<ArgTs>(Args)...));
}
/// Set the ControllerAccess object.
/// Initiate connection with controller.
///
/// Upon first call, assuming that the Session has not already been detached
/// or shutdown, this will take (shared) ownership of CA and call its connect
/// method.
///
/// If detach or shutdown have already been called then this method will not
/// take ownership of CA or call its connect method.
void attach(std::shared_ptr<ControllerAccess> CA);
/// Disconnect the ControllerAccess object.
void detach();
/// Initiate detach from the controller.
///
/// If attached, this will request disconnection from the controller and
/// then notify all Services via onDetach. The optional OnDetach callback
/// will be called once the detach is complete.
///
/// If the Session is already detached or shut down, the callback (if
/// provided) will be called immediately.
void detach(OnDetachFn OnDetach = {});
/// Initiate session shutdown.
///
/// Runs shutdown on registered resources in reverse order.
void shutdown(OnShutdownFn OnShutdown = {});
/// Initiate session shutdown and block until complete.
void waitForShutdown();
/// Register a callback to be called when the Session detaches from the
/// controller. If the Session has already detached, the callback will be
/// called immediately.
void addOnDetach(OnDetachFn OnDetach);
/// Register a callback to be called when the Session shuts down. If the
/// Session has already shut down, the callback will be called immediately.
void addOnShutdown(OnShutdownFn OnShutdown);
/// Call a tagged handler in the Controller.
///
/// This method can be called directly, but is expected to be more commonly
/// called by the WrapperFunction::call method using a CallViaSession object
/// (see below).
/// called via WrapperFunction::call using a CallViaSession object (returned
/// by the callViaSession method).
void callController(OnCallHandlerCompleteFn OnComplete, HandlerTag T,
WrapperFunctionBuffer ArgBytes) {
if (auto TmpCA = CA)
CA->callController(std::move(OnComplete), T, std::move(ArgBytes));
if (auto TmpCA = std::atomic_load(&CA))
TmpCA->callController(std::move(OnComplete), T, std::move(ArgBytes));
else
OnComplete(WrapperFunctionBuffer::createOutOfBandError(
"no controller attached"));
@ -210,14 +266,37 @@ public:
}
private:
struct ShutdownInfo {
bool Complete = false;
std::vector<std::unique_ptr<Service>> Services;
std::vector<OnShutdownCompleteFn> OnCompletes;
enum class State {
/// Used as a placeholder when there is no target state.
None,
/// The Session starts in this state.
Start,
/// Controller attached.
Attached,
/// Controller detached.
Detached,
/// Shutdown.
Shutdown
};
void shutdownNext();
void shutdownComplete();
class NotificationService;
NotificationService &addNotificationService();
void appendService(std::unique_ptr<Service> Srv);
void handleDisconnect();
void proceedToDetach(std::unique_lock<std::mutex> &Lock,
std::shared_ptr<ControllerAccess> TmpCA);
void detachServices(std::vector<Service *> ToNotify, bool ShutdownRequested);
void completeDetach();
void proceedToShutdown(std::unique_lock<std::mutex> &Lock);
void shutdownServices(std::vector<Service *> ToNotify);
void completeShutdown();
void handleWrapperCall(uint64_t CallId, orc_rt_WrapperFunction Fn,
WrapperFunctionBuffer ArgBytes) {
@ -227,7 +306,7 @@ private:
}
void sendWrapperResult(uint64_t CallId, WrapperFunctionBuffer ResultBytes) {
if (auto TmpCA = CA)
if (auto TmpCA = std::atomic_load(&CA))
TmpCA->sendWrapperResult(CallId, std::move(ResultBytes));
}
@ -240,8 +319,10 @@ private:
ErrorReporterFn ReportError;
mutable std::mutex M;
State CurrentState = State::Start;
State TargetState = State::None;
std::vector<std::unique_ptr<Service>> Services;
std::unique_ptr<ShutdownInfo> SI;
NotificationService &Notifiers;
};
} // namespace orc_rt

View File

@ -14,105 +14,348 @@
namespace orc_rt {
class Session::NotificationService : public Service {
public:
void addOnDetach(Session::OnDetachFn OnDetach) {
ToNotifyOnDetach.push_back(std::move(OnDetach));
}
void addOnShutdown(Session::OnShutdownFn OnShutdown) {
ToNotifyOnShutdown.push_back(std::move(OnShutdown));
}
void onDetach(OnCompleteFn OnComplete, bool ShutdownRequested) override {
while (!ToNotifyOnDetach.empty()) {
auto ToNotify = std::move(ToNotifyOnDetach.back());
ToNotifyOnDetach.pop_back();
ToNotify();
}
OnComplete();
}
void onShutdown(OnCompleteFn OnComplete) override {
while (!ToNotifyOnShutdown.empty()) {
auto ToNotify = std::move(ToNotifyOnShutdown.back());
ToNotifyOnShutdown.pop_back();
ToNotify();
}
OnComplete();
}
private:
std::vector<Session::OnDetachFn> ToNotifyOnDetach;
std::vector<Session::OnShutdownFn> ToNotifyOnShutdown;
};
Session::ControllerAccess::~ControllerAccess() = default;
Session::Session(ExecutorProcessInfo EPI,
std::unique_ptr<TaskDispatcher> Dispatcher,
ErrorReporterFn ReportError)
: EPI(std::move(EPI)), Dispatcher(std::move(Dispatcher)),
ReportError(std::move(ReportError)) {}
ReportError(std::move(ReportError)), Notifiers(addNotificationService()) {
}
Session::~Session() { waitForShutdown(); }
void Session::shutdown(OnShutdownCompleteFn OnShutdownComplete) {
assert(OnShutdownComplete && "OnShutdownComplete must be set");
// Safe to call concurrently / redundantly.
detach();
void Session::attach(std::shared_ptr<ControllerAccess> CA) {
assert(CA && "attach called with null CA object");
{
std::scoped_lock<std::mutex> Lock(M);
if (SI) {
// SI exists: someone called shutdown already. If the shutdown is not yet
// complete then just add OnShutdownComplete to the list of pending
// callbacks for the in-progress shutdown, then return.
// (If the shutdown is already complete then we'll run the handler
// directly below).
if (!SI->Complete)
return SI->OnCompletes.push_back(std::move(OnShutdownComplete));
// Controller can only be attached from the start state if no
// other operation has been requested.
if (CurrentState != State::Start || TargetState != State::None)
return;
assert(std::atomic_load(&this->CA) == nullptr &&
"ControllerAccess object already attached?");
std::atomic_store(&this->CA, CA);
TargetState = State::Attached;
}
CA->connect();
{
std::scoped_lock<std::mutex> Lock(M);
assert(TargetState >= State::Attached);
// There are three possibilities that we have to deal with here:
// 1. Connection succeeded and we're done.
//
// We just need to move to the Attached state, reset TargetState, and
// we're done.
//
// 2. Connect failed.
//
// In this case connect must have called handleDisconnect, which should
// have initiated the detach. We just need to bail out.
//
// 3. Connection succeeded but a detach or shutdown was requested
// concurrently. In this case we need to start the detach process.
//
// To distinguish between these we first look at the target state. If it's
// Attached then it's option (1) and we're done:
if (TargetState == State::Attached) {
CurrentState = State::Attached;
TargetState = State::None;
return;
}
// The target state is Detached or higher. Check the current state. If it's
// also Detached or higher then handleDisconnect must already have been
// called (in turn calling proceedToDetach, which updated the current
// state). In this case we're in option (2) and we just need to bail out.
if (CurrentState >= State::Detached)
return;
// The target state is Detached or higher, but the current state is still
// Start. Someone must have called detach / shutdown concurrently. This is
// option (3) and we just need to update the current state and run
// disconnect.
CurrentState = State::Attached;
}
CA->disconnect();
}
void Session::detach(OnDetachFn OnDetach) {
addOnDetach(std::move(OnDetach));
std::shared_ptr<ControllerAccess> TmpCA;
{
std::unique_lock<std::mutex> Lock(M);
// Check if someone's already managing transitions.
if (TargetState != State::None) {
TargetState = std::max(TargetState, State::Detached);
return;
}
// Nobody's managing transitions, but this request is redundant.
if (CurrentState >= State::Detached)
return;
// We've actually got work to do.
TargetState = State::Detached;
assert((CurrentState == State::Start || CurrentState == State::Attached) &&
"Unexpected current state");
if (CurrentState == State::Attached) {
assert(CA && "Attached, but not CA?");
TmpCA = std::atomic_load(&this->CA);
} else {
// SI does not exist: We're the first to call shutdown. Create a
// ShutdownInfo struct and add OnShutdownComplete to the list of pending
// callbacks, then call shutdownNext below (outside the lock).
SI = std::make_unique<ShutdownInfo>();
SI->OnCompletes.push_back(std::move(OnShutdownComplete));
std::swap(SI->Services, Services);
assert(CurrentState == State::Start);
proceedToDetach(Lock, std::atomic_exchange(&this->CA, {}));
return;
}
}
// OnShutdownComplete is set (i.e. not moved into the list of pending
// callbacks). This can only happen if shutdown is already complete. Call
// OnComplete directly and return.
if (OnShutdownComplete)
return OnShutdownComplete();
TmpCA->disconnect();
}
// OnShutdownComplete is _not_ set (i.e. was moved into the list of pending
// handlers), and we didn't return under the lock above, so we must be
// responsible for the shutdown. Call shutdownNext.
shutdownNext();
void Session::shutdown(OnShutdownFn OnShutdown) {
addOnShutdown(std::move(OnShutdown));
std::shared_ptr<ControllerAccess> TmpCA;
{
std::unique_lock<std::mutex> Lock(M);
// Check if someone's already managing transitions.
if (TargetState != State::None) {
TargetState = std::max(TargetState, State::Shutdown);
return;
}
// Nobody's managing transition, but this request is redundant.
if (CurrentState == State::Shutdown)
return;
TargetState = State::Shutdown;
assert((CurrentState == State::Start || CurrentState == State::Attached ||
CurrentState == State::Detached) &&
"Unexpected current state");
switch (CurrentState) {
case State::Start:
proceedToDetach(Lock, nullptr);
return;
case State::Attached:
TmpCA = std::atomic_load(&this->CA);
break;
case State::Detached:
proceedToShutdown(Lock);
return;
default:
assert(false && "Illegal state");
abort();
}
}
TmpCA->disconnect();
}
void Session::waitForShutdown() {
std::promise<void> P;
auto F = P.get_future();
shutdown([P = std::move(P)]() mutable { P.set_value(); });
addOnShutdown([P = std::move(P)]() mutable { P.set_value(); });
shutdown();
F.get();
}
void Session::attach(std::shared_ptr<ControllerAccess> CA) {
assert(CA && "Cannot attach null controller");
std::scoped_lock<std::mutex> Lock(M);
assert(!this->CA && "Cannot re-attach controller");
assert(!SI && "Cannot attach controller after shutdown");
this->CA = std::move(CA);
}
void Session::detach() {
if (auto TmpCA = CA) {
TmpCA->doDisconnect();
CA = nullptr;
}
}
void Session::shutdownNext() {
if (SI->Services.empty())
return shutdownComplete();
// Get the next Service to shut down.
auto NextSrv = std::move(SI->Services.back());
SI->Services.pop_back();
NextSrv->onShutdown([this]() { shutdownNext(); });
}
void Session::shutdownComplete() {
std::unique_ptr<TaskDispatcher> TmpDispatcher;
void Session::addOnDetach(OnDetachFn OnDetach) {
if (!OnDetach)
return;
{
std::scoped_lock<std::mutex> Lock(M);
TmpDispatcher = std::move(Dispatcher);
if (CurrentState < State::Detached) {
Notifiers.addOnDetach(std::move(OnDetach));
return;
}
}
// We've already detached. Run in-place.
OnDetach();
}
TmpDispatcher->shutdown();
std::vector<OnShutdownCompleteFn> OnCompletes;
void Session::addOnShutdown(OnShutdownFn OnShutdown) {
if (!OnShutdown)
return;
{
std::scoped_lock<std::mutex> Lock(M);
SI->Complete = true;
OnCompletes = std::move(SI->OnCompletes);
if (CurrentState < State::Shutdown) {
Notifiers.addOnShutdown(std::move(OnShutdown));
return;
}
}
// We've already shutdown. Run in-place.
OnShutdown();
}
Session::NotificationService &Session::addNotificationService() {
auto NS = std::make_unique<NotificationService>();
auto &TmpNS = *NS;
Services.push_back(std::move(NS));
return TmpNS;
}
void Session::appendService(std::unique_ptr<Service> Srv) {
bool ShuttingDown = false;
{
std::scoped_lock<std::mutex> Lock(M);
if (CurrentState < State::Detached) {
Services.push_back(std::move(Srv));
return;
}
ShuttingDown = TargetState == State::Shutdown;
}
for (auto &OnShutdownComplete : OnCompletes)
OnShutdownComplete();
// Already detached. Call onDetach on the service.
assert(Srv && "Should be non-null here");
Srv->onDetach([]() {}, ShuttingDown);
// Try to append again.
{
std::scoped_lock<std::mutex> Lock(M);
if (CurrentState < State::Shutdown) {
Services.push_back(std::move(Srv));
return;
}
}
// Already shutdown. Call onShutdown on the service.
assert(Srv && "Should be non-null here");
Srv->onShutdown([]() {});
// At this point the service has already been shut down, but we need to keep
// the object alive until the Session is destroyed, so append it anyway.
{
std::scoped_lock<std::mutex> Lock(M);
Services.push_back(std::move(Srv));
}
}
void Session::handleDisconnect() {
// If we get here we _don't_ need to call disconnect.
std::unique_lock<std::mutex> Lock(M);
assert(CurrentState <= State::Attached);
TargetState = std::max(TargetState, State::Detached);
proceedToDetach(Lock, std::atomic_exchange(&this->CA, {}));
}
void Session::proceedToDetach(std::unique_lock<std::mutex> &Lock,
std::shared_ptr<ControllerAccess> TmpCA) {
std::vector<Service *> ToNotify;
ToNotify.reserve(Services.size());
for (auto &Srv : Services)
ToNotify.push_back(Srv.get());
bool ShutdownRequested = TargetState == State::Shutdown;
CurrentState = State::Detached;
Lock.unlock();
// Throw away controller if present.
TmpCA.reset();
// Notify services.
detachServices(std::move(ToNotify), ShutdownRequested);
}
void Session::detachServices(std::vector<Service *> ToNotify,
bool ShutdownRequested) {
if (ToNotify.empty())
return completeDetach();
auto *Srv = ToNotify.back();
ToNotify.pop_back();
Srv->onDetach(
[this, ToNotify = std::move(ToNotify), ShutdownRequested]() {
detachServices(std::move(ToNotify), ShutdownRequested);
},
ShutdownRequested);
}
void Session::completeDetach() {
std::unique_lock<std::mutex> Lock(M);
assert(CurrentState == State::Detached);
if (TargetState == State::Detached) {
TargetState = State::None;
return;
}
// Someone must have requested shutdown.
assert(TargetState == State::Shutdown);
proceedToShutdown(Lock);
}
void Session::proceedToShutdown(std::unique_lock<std::mutex> &Lock) {
std::vector<Service *> ToNotify;
ToNotify.reserve(Services.size());
for (auto &Srv : Services)
ToNotify.push_back(Srv.get());
CurrentState = State::Shutdown;
Lock.unlock();
// Notify services.
shutdownServices(std::move(ToNotify));
}
void Session::shutdownServices(std::vector<Service *> ToNotify) {
if (ToNotify.empty())
return completeShutdown();
auto *Srv = ToNotify.back();
ToNotify.pop_back();
Srv->onShutdown([this, ToNotify = std::move(ToNotify)]() {
shutdownServices(std::move(ToNotify));
});
}
void Session::completeShutdown() {
Dispatcher->shutdown();
std::unique_lock<std::mutex> Lock(M);
assert(CurrentState == State::Shutdown);
assert(TargetState == State::Shutdown);
TargetState = State::None;
}
void Session::wrapperReturn(orc_rt_SessionRef S, uint64_t CallId,

View File

@ -112,10 +112,13 @@ class MockControllerAccess : public Session::ControllerAccess {
public:
MockControllerAccess(Session &SS) : Session::ControllerAccess(SS), SS(SS) {}
void connect() override {}
void disconnect() override {
std::unique_lock<std::mutex> Lock(M);
Shutdown = true;
ShutdownCV.wait(Lock, [this]() { return Shutdown && Outstanding == 0; });
notifyDisconnected();
}
void callController(OnCallHandlerCompleteFn OnComplete, HandlerTag T,
@ -311,9 +314,9 @@ TEST(SessionTest, SingleService) {
std::make_unique<MockService>(DetachOpIdx, ShutdownOpIdx, OpIdx));
}
EXPECT_EQ(OpIdx, 1U);
EXPECT_EQ(DetachOpIdx, std::nullopt);
EXPECT_THAT(ShutdownOpIdx, Optional(Eq(0)));
EXPECT_EQ(OpIdx, 2U);
EXPECT_EQ(DetachOpIdx, 0U);
EXPECT_EQ(ShutdownOpIdx, 1U);
}
TEST(SessionTest, MultipleServices) {
@ -329,11 +332,11 @@ TEST(SessionTest, MultipleServices) {
ShutdownOpIdx[I], OpIdx));
}
EXPECT_EQ(OpIdx, 3U);
EXPECT_EQ(OpIdx, 6U);
// Expect shutdown in reverse order.
for (size_t I = 0; I != 3; ++I) {
EXPECT_EQ(DetachOpIdx[I], std::nullopt);
EXPECT_THAT(ShutdownOpIdx[I], Optional(Eq(2 - I)));
EXPECT_EQ(DetachOpIdx[I], 2 - I);
EXPECT_EQ(ShutdownOpIdx[I], 5 - I);
}
}
@ -355,8 +358,8 @@ TEST(SessionTest, ExpectedShutdownSequence) {
Tasks,
[&]() {
EXPECT_TRUE(ShutdownOpIdx);
EXPECT_EQ(*ShutdownOpIdx, 0);
EXPECT_FALSE(SessionShutdownComplete);
EXPECT_EQ(*ShutdownOpIdx, 1);
EXPECT_TRUE(SessionShutdownComplete);
DispatcherShutDown = true;
}),
noErrors);
@ -364,7 +367,7 @@ TEST(SessionTest, ExpectedShutdownSequence) {
std::make_unique<MockService>(DetachOpIdx, ShutdownOpIdx, OpIdx));
S.shutdown([&]() {
EXPECT_TRUE(DispatcherShutDown);
EXPECT_FALSE(DispatcherShutDown);
SessionShutdownComplete = true;
});
S.waitForShutdown();