@@ -36,6 +36,7 @@ using executorch::extension::llm::batching::FinishReason;
3636using executorch::extension::llm::batching::GenConfig;
3737using executorch::extension::llm::batching::GenerationHandle;
3838using executorch::extension::llm::batching::GenerationUpdate;
39+ using executorch::extension::llm::batching::InitializationState;
3940using executorch::extension::llm::batching::Position;
4041using executorch::extension::llm::batching::Runner;
4142using executorch::extension::llm::batching::SamplingParams;
@@ -2668,19 +2669,189 @@ TEST(InitializeTest, FailureStopsTheRunnerAndRefusesSessions) {
26682669 << " a session must not be opened on an executor that failed to start" ;
26692670}
26702671
2671- // Opens queued while the engine was starting still have to be answered, or a
2672- // caller blocked on the future would hang.
2673- TEST (InitializeTest, FailureDoesNotStrandAQueuedOpen) {
2674- FakeExecutor executor;
2675- executor.fail_initialize = true ;
2672+ namespace {
2673+
2674+ enum class InitializeOutcome {
2675+ Success,
2676+ Failure,
2677+ #if ET_HAS_EXCEPTIONS
2678+ Exception,
2679+ NonStandardException,
2680+ #endif
2681+ };
2682+
2683+ class GatedInitializeExecutor : public FakeExecutor {
2684+ public:
2685+ explicit GatedInitializeExecutor (InitializeOutcome outcome)
2686+ : outcome_(outcome) {
2687+ fail_initialize = outcome_ == InitializeOutcome::Failure;
2688+ }
2689+
2690+ bool initialize () override {
2691+ const bool ready = FakeExecutor::initialize ();
2692+ {
2693+ std::unique_lock<std::mutex> lock (mutex_);
2694+ entered_ = true ;
2695+ cv_.notify_all ();
2696+ cv_.wait (lock, [this ] { return released_; });
2697+ }
2698+ #if ET_HAS_EXCEPTIONS
2699+ if (outcome_ == InitializeOutcome::Exception) {
2700+ throw std::runtime_error (" initialize failed" );
2701+ }
2702+ if (outcome_ == InitializeOutcome::NonStandardException) {
2703+ throw 7 ;
2704+ }
2705+ #endif
2706+ return ready;
2707+ }
2708+
2709+ bool wait_for_initialize () {
2710+ std::unique_lock<std::mutex> lock (mutex_);
2711+ return cv_.wait_for (lock, kTimeout , [this ] { return entered_; });
2712+ }
2713+
2714+ void release_initialize () {
2715+ {
2716+ std::lock_guard<std::mutex> lock (mutex_);
2717+ released_ = true ;
2718+ }
2719+ cv_.notify_all ();
2720+ }
2721+
2722+ private:
2723+ const InitializeOutcome outcome_;
2724+ std::mutex mutex_;
2725+ std::condition_variable cv_;
2726+ bool entered_ = false ;
2727+ bool released_ = false ;
2728+ };
2729+
2730+ class InitializeStateTest : public ::testing::TestWithParam<InitializeOutcome> {
2731+ };
2732+
2733+ } // namespace
2734+
2735+ TEST_P (InitializeStateTest, PendingSettlesQueuedOpensAndRetainsResult) {
2736+ GatedInitializeExecutor executor (GetParam ());
26762737 Fixture fixture (executor);
2738+ // Keep assertions nonfatal until the gate is released so teardown can join.
2739+ EXPECT_TRUE (executor.wait_for_initialize ());
2740+ EXPECT_EQ (
2741+ fixture.runner .initialization_state (), InitializationState::Pending);
26772742
26782743 std::vector<std::future<std::optional<Session>>> opens;
26792744 for (int i = 0 ; i < 4 ; ++i) {
26802745 opens.push_back (fixture.runner .open_session_async ());
2746+ EXPECT_EQ (
2747+ opens.back ().wait_for (std::chrono::seconds (0 )),
2748+ std::future_status::timeout);
26812749 }
2750+ EXPECT_TRUE (executor.opened ().empty ());
2751+ EXPECT_EQ (
2752+ fixture.runner .initialization_state (), InitializationState::Pending);
2753+ executor.release_initialize ();
2754+
2755+ const bool succeeds = GetParam () == InitializeOutcome::Success;
2756+ const auto expected =
2757+ succeeds ? InitializationState::Ready : InitializationState::Failed;
26822758 for (auto & opened : opens) {
26832759 ASSERT_EQ (opened.wait_for (kTimeout ), std::future_status::ready);
2684- EXPECT_FALSE (opened.get ().has_value ());
2760+ auto session = opened.get ();
2761+ EXPECT_EQ (session.has_value (), succeeds);
2762+ EXPECT_EQ (fixture.runner .initialization_state (), expected);
2763+ if (session) {
2764+ EXPECT_TRUE (session->valid ());
2765+ }
2766+ }
2767+ if (!succeeds) {
2768+ auto refused = fixture.runner .open_session_async ();
2769+ ASSERT_EQ (
2770+ refused.wait_for (std::chrono::seconds (0 )), std::future_status::ready);
2771+ EXPECT_FALSE (refused.get ());
2772+ }
2773+
2774+ fixture.runner .shutdown ();
2775+ EXPECT_EQ (fixture.runner .initialization_state (), expected);
2776+ fixture.runner .shutdown ();
2777+ EXPECT_EQ (fixture.runner .initialization_state (), expected);
2778+ auto refused = fixture.runner .open_session_async ();
2779+ ASSERT_EQ (
2780+ refused.wait_for (std::chrono::seconds (0 )), std::future_status::ready);
2781+ EXPECT_FALSE (refused.get ());
2782+ EXPECT_EQ (executor.initialize_calls (), 1 );
2783+ EXPECT_FALSE (executor.called_before_initialize ());
2784+ EXPECT_EQ (executor.opened ().size (), succeeds ? 4u : 0u );
2785+ EXPECT_EQ (executor.closed ().size (), executor.opened ().size ());
2786+ EXPECT_EQ (executor.open_count (), 0 );
2787+ EXPECT_EQ (fixture.runner .metrics ().steps , 0u );
2788+ }
2789+
2790+ TEST_P (
2791+ InitializeStateTest,
2792+ ShutdownWhilePendingStillRetainsInitializationResult) {
2793+ GatedInitializeExecutor executor (GetParam ());
2794+ Fixture fixture (executor);
2795+ EXPECT_TRUE (executor.wait_for_initialize ());
2796+ std::vector<std::future<std::optional<Session>>> opens;
2797+ opens.push_back (fixture.runner .open_session_async ());
2798+ EXPECT_EQ (
2799+ opens.back ().wait_for (std::chrono::seconds (0 )),
2800+ std::future_status::timeout);
2801+
2802+ std::thread stopping ([&] { fixture.runner .shutdown (); });
2803+ bool stopped_admission = false ;
2804+ const auto deadline = std::chrono::steady_clock::now () + kTimeout ;
2805+ while (std::chrono::steady_clock::now () < deadline) {
2806+ auto probe = fixture.runner .open_session_async ();
2807+ if (probe.wait_for (std::chrono::milliseconds (1 )) ==
2808+ std::future_status::ready) {
2809+ stopped_admission = !probe.get ();
2810+ break ;
2811+ }
2812+ opens.push_back (std::move (probe));
26852813 }
2814+ EXPECT_TRUE (stopped_admission);
2815+ EXPECT_EQ (
2816+ fixture.runner .initialization_state (), InitializationState::Pending);
2817+ executor.release_initialize ();
2818+ stopping.join ();
2819+
2820+ for (auto & opened : opens) {
2821+ ASSERT_EQ (opened.wait_for (kTimeout ), std::future_status::ready);
2822+ EXPECT_FALSE (opened.get ());
2823+ }
2824+ EXPECT_EQ (
2825+ fixture.runner .initialization_state (),
2826+ GetParam () == InitializeOutcome::Success ? InitializationState::Ready
2827+ : InitializationState::Failed);
2828+ EXPECT_EQ (executor.initialize_calls (), 1 );
2829+ EXPECT_TRUE (executor.opened ().empty ());
2830+ EXPECT_EQ (fixture.runner .metrics ().steps , 0u );
26862831}
2832+
2833+ INSTANTIATE_TEST_SUITE_P (
2834+ InitializeTest,
2835+ InitializeStateTest,
2836+ ::testing::Values (
2837+ InitializeOutcome::Success,
2838+ #if ET_HAS_EXCEPTIONS
2839+ InitializeOutcome::Exception,
2840+ InitializeOutcome::NonStandardException,
2841+ #endif
2842+ InitializeOutcome::Failure),
2843+ [](const ::testing::TestParamInfo<InitializeOutcome>& info) {
2844+ switch (info.param ) {
2845+ case InitializeOutcome::Success:
2846+ return " Success" ;
2847+ case InitializeOutcome::Failure:
2848+ return " Failure" ;
2849+ #if ET_HAS_EXCEPTIONS
2850+ case InitializeOutcome::Exception:
2851+ return " Exception" ;
2852+ case InitializeOutcome::NonStandardException:
2853+ return " NonStandardException" ;
2854+ #endif
2855+ }
2856+ return " Unknown" ;
2857+ });
0 commit comments