//===- SessionTest.cpp ----------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // Tests for orc-rt's Session.h APIs. // //===----------------------------------------------------------------------===// #include "orc-rt/Session.h" #include "orc-rt/SPSWrapperFunction.h" #include "orc-rt/ThreadPoolTaskDispatcher.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include "CommonTestUtils.h" #include #include #include #include using namespace orc_rt; using ::testing::Eq; using ::testing::Optional; class MockService : public Service { public: enum class Op { Detach, Shutdown }; static void noop(Op) {} MockService(std::optional &DetachOpIdx, std::optional &ShutdownOpIdx, size_t &OpIdx, move_only_function GenResult = noop) : DetachOpIdx(DetachOpIdx), ShutdownOpIdx(ShutdownOpIdx), OpIdx(OpIdx), GenResult(std::move(GenResult)) {} void onDetach(OnCompleteFn OnComplete, bool ShutdownRequested) override { DetachOpIdx = OpIdx++; GenResult(Op::Detach); OnComplete(); } void onShutdown(OnCompleteFn OnComplete) override { ShutdownOpIdx = OpIdx++; GenResult(Op::Shutdown); OnComplete(); } private: std::optional &DetachOpIdx; std::optional &ShutdownOpIdx; size_t &OpIdx; move_only_function GenResult; }; class ConfigurableService : public Service { public: ConfigurableService(int ConstructorOption) {} /// Fallible named constructor for testing tryCreateService. static Expected> Create(bool Fail) { if (Fail) return make_error("failed to create service"); return std::make_unique(42); } void onDetach(OnCompleteFn OnComplete, bool ShutdownRequested) override { OnComplete(); } void onShutdown(OnCompleteFn OnComplete) override { OnComplete(); } void doMoreConfig(int) noexcept {} }; class EnqueueingDispatcher : public TaskDispatcher { public: using OnShutdownRunFn = move_only_function; EnqueueingDispatcher(std::deque> &Tasks, OnShutdownRunFn OnShutdownRun = {}) : Tasks(Tasks), OnShutdownRun(std::move(OnShutdownRun)) {} void dispatch(std::unique_ptr T) override { Tasks.push_back(std::move(T)); } void shutdown() override { if (OnShutdownRun) OnShutdownRun(); } /// Run up to NumTasks (arbitrarily many if NumTasks == std::nullopt) tasks /// from the front of the queue, returning the number actually run. static size_t runTasksFromFront(std::deque> &Tasks, std::optional NumTasks = std::nullopt) { size_t NumRun = 0; while (!Tasks.empty() && (!NumTasks || NumRun != *NumTasks)) { auto T = std::move(Tasks.front()); Tasks.pop_front(); T->run(); ++NumRun; } return NumRun; } private: std::deque> &Tasks; OnShutdownRunFn OnShutdownRun; }; class MockControllerAccess : public Session::ControllerAccess { public: using OnConnectFn = move_only_function; MockControllerAccess(Session &SS) : Session::ControllerAccess(SS), SS(SS) {} void setOnConnect(OnConnectFn OnConnect) { this->OnConnect = std::move(OnConnect); } void connect(BootstrapInfo BI) override { if (OnConnect) OnConnect(BI); } void disconnect() override { std::unique_lock Lock(M); Shutdown = true; ShutdownCV.wait(Lock, [this]() { return Shutdown && Outstanding == 0; }); notifyDisconnected(); } void callController(OnCallHandlerCompleteFn OnComplete, HandlerTag T, WrapperFunctionBuffer ArgBytes) override { // Simulate a call to the controller by dispatching a task to run the // requested function. size_t CId; { std::scoped_lock Lock(M); if (Shutdown) return; CId = CallId++; Pending[CId] = std::move(OnComplete); ++Outstanding; } SS.dispatch(makeGenericTask([this, CId, OnComplete = std::move(OnComplete), T, ArgBytes = std::move(ArgBytes)]() mutable { auto Fn = reinterpret_cast(T); Fn(reinterpret_cast(this), CId, wfReturn, ArgBytes.release()); })); bool Notify = false; { std::scoped_lock Lock(M); if (--Outstanding == 0 && Shutdown) Notify = true; } if (Notify) ShutdownCV.notify_all(); } void sendWrapperResult(uint64_t CallId, WrapperFunctionBuffer ResultBytes) override { // Respond to a simulated call by the controller. OnCallHandlerCompleteFn OnComplete; { std::scoped_lock Lock(M); if (Shutdown) { assert(Pending.empty() && "Shut down but results still pending?"); return; } auto I = Pending.find(CallId); assert(I != Pending.end()); OnComplete = std::move(I->second); Pending.erase(I); ++Outstanding; } SS.dispatch( makeGenericTask([OnComplete = std::move(OnComplete), ResultBytes = std::move(ResultBytes)]() mutable { OnComplete(std::move(ResultBytes)); })); bool Notify = false; { std::scoped_lock Lock(M); if (--Outstanding == 0 && Shutdown) Notify = true; } if (Notify) ShutdownCV.notify_all(); } void callFromController(OnCallHandlerCompleteFn OnComplete, orc_rt_WrapperFunction Fn, WrapperFunctionBuffer ArgBytes) { size_t CId = 0; bool BailOut = false; { std::scoped_lock Lock(M); if (!Shutdown) { CId = CallId++; Pending[CId] = std::move(OnComplete); ++Outstanding; } else BailOut = true; } if (BailOut) return OnComplete(WrapperFunctionBuffer::createOutOfBandError( "Controller disconnected")); handleWrapperCall(CId, Fn, std::move(ArgBytes)); bool Notify = false; { std::scoped_lock Lock(M); if (--Outstanding == 0 && Shutdown) Notify = true; } if (Notify) ShutdownCV.notify_all(); } /// Simulate start of outstanding operation. void incOutstanding() { std::scoped_lock Lock(M); ++Outstanding; } /// Simulate end of outstanding operation. void decOutstanding() { bool Notify = false; { std::scoped_lock Lock(M); if (--Outstanding == 0 && Shutdown) Notify = true; } if (Notify) ShutdownCV.notify_all(); } private: static void wfReturn(orc_rt_SessionRef S, uint64_t CallId, orc_rt_WrapperFunctionBuffer ResultBytes) { // Abuse "session" to refer to the ControllerAccess object. // We can just re-use sendFunctionResult for this. reinterpret_cast(S)->sendWrapperResult( CallId, WrapperFunctionBuffer(ResultBytes)); } Session &SS; std::mutex M; bool Shutdown = false; size_t Outstanding = 0; size_t CallId = 0; std::unordered_map Pending; std::condition_variable ShutdownCV; OnConnectFn OnConnect; }; class CallViaMockControllerAccess { public: CallViaMockControllerAccess(MockControllerAccess &CA, orc_rt_WrapperFunction Fn) : CA(CA), Fn(Fn) {} void operator()(Session::OnCallHandlerCompleteFn OnComplete, WrapperFunctionBuffer ArgBytes) { CA.callFromController(std::move(OnComplete), Fn, std::move(ArgBytes)); } private: MockControllerAccess &CA; orc_rt_WrapperFunction Fn; }; void waitForShutdown(Session &S) { std::promise P; auto F = P.get_future(); S.shutdown([P = std::move(P)]() mutable { P.set_value(); }); F.get(); } TEST(SessionTest, TrivialConstructionAndDestruction) { Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); } TEST(SessionTest, ReportError) { Error E = Error::success(); cantFail(std::move(E)); // Force error into checked state. Session S(mockExecutorProcessInfo(), std::make_unique(), [&](Error Err) { E = std::move(Err); }); S.reportError(make_error("foo")); if (E) EXPECT_EQ(toString(std::move(E)), "foo"); else ADD_FAILURE() << "Missing error value"; } TEST(SessionTest, DispatchTask) { int X = 0; std::deque> Tasks; Session S(mockExecutorProcessInfo(), std::make_unique(Tasks), noErrors); EXPECT_EQ(Tasks.size(), 0U); S.dispatch(makeGenericTask([&]() { ++X; })); EXPECT_EQ(Tasks.size(), 1U); auto T = std::move(Tasks.front()); Tasks.pop_front(); T->run(); EXPECT_EQ(X, 1); } TEST(SessionTest, SingleService) { size_t OpIdx = 0; std::optional DetachOpIdx; std::optional ShutdownOpIdx; { Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); S.addService( std::make_unique(DetachOpIdx, ShutdownOpIdx, OpIdx)); } EXPECT_EQ(OpIdx, 2U); EXPECT_EQ(DetachOpIdx, 0U); EXPECT_EQ(ShutdownOpIdx, 1U); } TEST(SessionTest, MultipleServices) { size_t OpIdx = 0; std::optional DetachOpIdx[3]; std::optional ShutdownOpIdx[3]; { Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); for (size_t I = 0; I != 3; ++I) S.addService(std::make_unique(DetachOpIdx[I], ShutdownOpIdx[I], OpIdx)); } EXPECT_EQ(OpIdx, 6U); // Expect shutdown in reverse order. for (size_t I = 0; I != 3; ++I) { EXPECT_EQ(DetachOpIdx[I], 2 - I); EXPECT_EQ(ShutdownOpIdx[I], 5 - I); } } TEST(SessionTest, ScheduleShutdownFromOnDetachHandler) { // Check that when we schedule a shutdown from an onDetach handler: // 1. The shutdown is scheduled. // 2. All onDetach handlers run before any onShutdown handlers. Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); int OnDetachHandlersRun = 0; bool OnShutdownHandlerRun = false; S.addOnDetach([&]() { ++OnDetachHandlersRun; }); S.addOnDetach([&]() { S.shutdown(); }); S.addOnDetach([&]() { ++OnDetachHandlersRun; }); S.addOnShutdown([&]() { EXPECT_EQ(OnDetachHandlersRun, 2); OnShutdownHandlerRun = true; }); S.detach(); EXPECT_TRUE(OnShutdownHandlerRun); } TEST(SessionTest, RedundantAsyncShutdown) { // Check that redundant calls to shutdown have their callbacks run. std::deque> Tasks; Session S(mockExecutorProcessInfo(), std::make_unique(Tasks), noErrors); // Initiate shutdown here, and wait for the on-shutdown callbacks to start // running. waitForShutdown(S); // Now try to add a new on-shutdown callback and verify that it runs. bool RedundantCallbackRan = false; S.shutdown([&]() { RedundantCallbackRan = true; }); EXPECT_TRUE(RedundantCallbackRan); } TEST(SessionTest, ExpectedShutdownSequenceWithNoActiveManagedCodeCalls) { // Check that Session shutdown results in... // 1. Services being shut down. // 2. The TaskDispatcher being shut down. // 3. A call to OnShutdownComplete. size_t OpIdx = 0; std::optional DetachOpIdx; std::optional ShutdownOpIdx; bool DispatcherShutDown = false; bool SessionShutdownComplete = false; { std::deque> Tasks; Session S(mockExecutorProcessInfo(), std::make_unique( Tasks, [&]() { EXPECT_TRUE(ShutdownOpIdx); EXPECT_EQ(*ShutdownOpIdx, 1); EXPECT_TRUE(SessionShutdownComplete); DispatcherShutDown = true; }), noErrors); S.addService( std::make_unique(DetachOpIdx, ShutdownOpIdx, OpIdx)); S.shutdown([&]() { EXPECT_FALSE(DispatcherShutDown); SessionShutdownComplete = true; }); } EXPECT_TRUE(SessionShutdownComplete); } TEST(SessionTest, ActiveManagedCallsDelayShutdown) { std::deque> Tasks; Session S(mockExecutorProcessInfo(), std::make_unique(Tasks), noErrors); size_t OpIdx = 0; std::optional DetachOpIdx; std::optional ShutdownOpIdx; S.createService(DetachOpIdx, ShutdownOpIdx, OpIdx); ASSERT_FALSE(DetachOpIdx); ASSERT_FALSE(ShutdownOpIdx); // Take a managed code call token. This should succeed. auto Tok = TaskGroup::Token(S.managedCodeTaskGroup()); ASSERT_TRUE(Tok); // We expect shutdown to wait for any active managed calls to complete. bool ShutdownComplete = false; S.shutdown([&]() { ShutdownComplete = true; }); // Detach should have happened, but shutdown should be waiting on token. EXPECT_EQ(DetachOpIdx, 0U); EXPECT_FALSE(ShutdownOpIdx); EXPECT_FALSE(ShutdownComplete); // The managed calls code group should have been closed. Assert that we // can't get a new token. ASSERT_FALSE(TaskGroup::Token(S.managedCodeTaskGroup())); Tok = TaskGroup::Token(); // Reset token. EXPECT_EQ(ShutdownOpIdx, 1U); EXPECT_TRUE(ShutdownComplete); } static void managedSyncVoidFunction(int *P) { *P = 42; } TEST(SessionTest, SyncCallManagedCodeVoidFn) { // Test synchronous calls to a void function while holding a // ManagedCodeTaskGroup token. Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); { // Pre-shutdown we expect token acquisition to succeed and the function to // run. int X = 0; bool CallSucceeded = S.callManagedCodeSync(managedSyncVoidFunction, &X); EXPECT_TRUE(CallSucceeded); EXPECT_EQ(X, 42U); } waitForShutdown(S); { // Post-shutdown we expect token acquisition to fail, and // callManagedCodeSync to return false. int X = 0; bool CallSucceeded = S.callManagedCodeSync(managedSyncVoidFunction, &X); EXPECT_FALSE(CallSucceeded); } } static int managedSyncNonVoidFunction(int N) { return N + 1; } TEST(SessionTest, SyncCallManagedCodeNonVoidFn) { // Test synchronous calls to a non-void function while holding a // ManagedCodeTaskGroup token. Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); { // Pre-shutdown we expect token acquisition to succeed, the function to be // run, and the result to be returned. auto Result = S.callManagedCodeSync(managedSyncNonVoidFunction, 41); EXPECT_TRUE(Result); EXPECT_EQ(*Result, 42U); } waitForShutdown(S); { // Post-shutdown we expect token acquisition to fail, and // callManagedCodeSync to return std::nullopt. auto Result = S.callManagedCodeSync(managedSyncNonVoidFunction, 41); EXPECT_EQ(Result, std::nullopt); } } static void managedAsyncVoidFunction(move_only_function Return, int *P) { *P = 42; Return(); } TEST(SessionTest, AsyncCallManagedCodeVoidFn) { // Test asynchronous calls to a void function while holding a // ManagedCodeTaskGroup token. Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); { // Pre-shutdown we expect token acquisition to succeed, and the function // and Return callback to be run. int X = 0; bool ReturnSucceeded = false; S.callManagedCodeAsync([&](bool B) { ReturnSucceeded = B; }, managedAsyncVoidFunction, &X); EXPECT_TRUE(ReturnSucceeded); EXPECT_EQ(X, 42U); } waitForShutdown(S); { // Post-shutdown we expect token acquisition to fail. Return should be // with `false` and the function should not be called. int X = 0; bool ReturnSucceeded = false; S.callManagedCodeAsync([&](bool B) { ReturnSucceeded = B; }, managedAsyncVoidFunction, &X); EXPECT_FALSE(ReturnSucceeded); EXPECT_EQ(X, 0U); } } static void managedAsyncNonVoidFunction(move_only_function Return, int *P) { Return(++*P); } TEST(SessionTest, AsyncCallManagedCodeNonVoidFn) { // Test asynchronous calls to a non-void function while holding a // ManagedCodeTaskGroup token. Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); { // Pre-shutdown we expect token acquisition to succeed, and the function // and Return callback to be run. int N = 41; std::optional Result; S.callManagedCodeAsync([&](std::optional N) { Result = N; }, managedAsyncNonVoidFunction, &N); EXPECT_TRUE(Result); EXPECT_EQ(*Result, 42U); EXPECT_EQ(N, 42U); } waitForShutdown(S); { // Post-shutdown we expect token acquisition to fail. Return should be // with `std::nullopt` and the function should not be called. int N = 41; std::optional Result; S.callManagedCodeAsync([&](std::optional N) { Result = N; }, managedAsyncNonVoidFunction, &N); EXPECT_EQ(Result, std::nullopt); EXPECT_EQ(N, 41U); } } TEST(SessionTest, AsyncCallManagedCodeHoldsTokenAcrossAsyncGap) { // Verify that the ManagedCodeTaskGroup token is held until the async // continuation runs, not just until callManagedCodeAsync returns. This // ensures shutdown blocks for the duration of the actual async work. Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); size_t OpIdx = 0; std::optional DetachOpIdx; std::optional ShutdownOpIdx; S.createService(DetachOpIdx, ShutdownOpIdx, OpIdx); // The managed code function stashes its continuation instead of calling it. std::optional Result; move_only_function StashedContinuation; S.callManagedCodeAsync([&](std::optional N) { Result = std::move(N); }, [&](move_only_function Return, int N) { // Stash the continuation and return without calling // it. StashedContinuation = std::move(Return); }, 41); // callManagedCodeAsync has returned, but the continuation hasn't been // called yet. The token should still be held inside StashedContinuation. ASSERT_TRUE(StashedContinuation); // Request shutdown. It should detach but block on the outstanding token. bool ShutdownComplete = false; S.shutdown([&]() { ShutdownComplete = true; }); EXPECT_EQ(DetachOpIdx, 0U); EXPECT_FALSE(ShutdownOpIdx); EXPECT_FALSE(ShutdownComplete); // Now invoke the stashed continuation and then destroy it, releasing the // token. StashedContinuation(42); StashedContinuation = {}; // Check result. EXPECT_EQ(Result, 42); // Shutdown should now have completed. EXPECT_EQ(ShutdownOpIdx, 1U); EXPECT_TRUE(ShutdownComplete); } TEST(SessionTest, AddServiceAndUseRef) { Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); auto &CS = S.addService(std::make_unique(42)); CS.doMoreConfig(1); } TEST(SessionTest, CreateServiceAndUseRef) { Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); auto &CS = S.createService(42); CS.doMoreConfig(1); } TEST(SessionTest, TryCreateServiceSuccess) { Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); auto CS = S.tryCreateService(false); if (auto Err = CS.takeError()) { ADD_FAILURE() << "expected service creation to succeed"; consumeError(std::move(Err)); } } TEST(SessionTest, TryCreateServiceFailure) { Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); auto CS = S.tryCreateService(true); if (auto Err = CS.takeError()) consumeError(std::move(Err)); else ADD_FAILURE() << "expected service creation to fail"; } TEST(ControllerAccessTest, Basics) { // Test that we can set the ControllerAccess implementation and still shut // down as expected. std::deque> Tasks; Session S(mockExecutorProcessInfo(), std::make_unique(Tasks), noErrors); S.attach(BootstrapInfo(S), S); EnqueueingDispatcher::runTasksFromFront(Tasks); } static void add_sps_wrapper(orc_rt_SessionRef S, uint64_t CallId, orc_rt_WrapperFunctionReturn Return, orc_rt_WrapperFunctionBuffer ArgBytes) { SPSWrapperFunction::handle( S, CallId, Return, ArgBytes, [](move_only_function Return, int32_t X, int32_t Y) { Return(X + Y); }); } TEST(ControllerAccessTest, ValidCallToController) { // Simulate a call to a controller handler. std::deque> Tasks; Session S(mockExecutorProcessInfo(), std::make_unique(Tasks), noErrors); S.attach(BootstrapInfo(S), S); int32_t Result = 0; SPSWrapperFunction::call( S.callViaSession(reinterpret_cast(add_sps_wrapper)), [&](Expected R) { Result = cantFail(std::move(R)); }, 41, 1); EnqueueingDispatcher::runTasksFromFront(Tasks); EXPECT_EQ(Result, 42); } TEST(ControllerAccessTest, CallToControllerBeforeAttach) { // Expect calls to the controller prior to attaching to fail. std::deque> Tasks; Session S(mockExecutorProcessInfo(), std::make_unique(Tasks), noErrors); Error Err = Error::success(); SPSWrapperFunction::call( S.callViaSession(reinterpret_cast(add_sps_wrapper)), [&](Expected R) { ErrorAsOutParameter _(Err); Err = R.takeError(); }, 41, 1); EXPECT_EQ(toString(std::move(Err)), "no controller attached"); } TEST(ControllerAccessTest, CallToControllerAfterDetach) { // Expect calls to the controller prior to attaching to fail. std::deque> Tasks; Session S(mockExecutorProcessInfo(), std::make_unique(Tasks), noErrors); S.attach(BootstrapInfo(S), S); S.detach(); Error Err = Error::success(); SPSWrapperFunction::call( S.callViaSession(reinterpret_cast(add_sps_wrapper)), [&](Expected R) { ErrorAsOutParameter _(Err); Err = R.takeError(); }, 41, 1); EXPECT_EQ(toString(std::move(Err)), "no controller attached"); } TEST(ControllerAccessTest, CallFromController) { // Simulate a call from the controller. std::deque> Tasks; Session S(mockExecutorProcessInfo(), std::make_unique(Tasks), noErrors); auto CA = std::make_shared(S); S.attach(CA, BootstrapInfo(S)); int32_t Result = 0; SPSWrapperFunction::call( CallViaMockControllerAccess(*CA, add_sps_wrapper), [&](Expected R) { Result = cantFail(std::move(R)); }, 41, 1); EnqueueingDispatcher::runTasksFromFront(Tasks); EXPECT_EQ(Result, 42); } TEST(ControllerAccessTest, BootstrapInfoPassedToConnect) { Session S(mockExecutorProcessInfo(), std::make_unique(), noErrors); // Test values. constexpr const char *SymName = "test_sym"; const char Sym = '.'; constexpr const char *SecretKey = "luggage_combo"; constexpr const char *SecretValue = "12345"; // Build a BootstrapInfo with custom symbols and values. BootstrapInfo BI(S); std::pair TestSyms[] = { {SymName, static_cast(&Sym)}}; cantFail(BI.symbols().addUnique(TestSyms)); BI.values()[SecretKey] = SecretValue; bool OnConnectRan = false; auto CA = std::make_shared(S); CA->setOnConnect([&](BootstrapInfo &BI) { EXPECT_EQ(BI.symbols().at(SymName), static_cast(&Sym)); EXPECT_EQ(BI.values().at(SecretKey), SecretValue); OnConnectRan = true; }); S.attach(CA, std::move(BI)); ASSERT_TRUE(OnConnectRan); }