|
28 | 28 | #include "butil/time.h" |
29 | 29 | #include "common/brpc/brpc_stub_cache.h" |
30 | 30 | #include "compute_env/pipeline/pipeline_timer_context.h" |
| 31 | +#include "exec/runtime/pipeline_driver.h" |
31 | 32 | #include "exec_primitive/pipeline/primitives/pipeline_observer.h" |
32 | 33 | #include "gtest/gtest.h" |
33 | 34 | #include "runtime/runtime_state.h" |
@@ -73,6 +74,18 @@ class LightProbe final : public LightTimerTask { |
73 | 74 | std::atomic<bool> ran{false}; |
74 | 75 | }; |
75 | 76 |
|
| 77 | +// Reaches PipelineDriver's protected default constructor and its protected global-RF-timer members |
| 78 | +// so a test can drive the destructor cleanup path without standing up a full fragment. |
| 79 | +class TimerTestPipelineDriver final : public PipelineDriver { |
| 80 | +public: |
| 81 | + TimerTestPipelineDriver() = default; |
| 82 | + |
| 83 | + void register_global_rf_timer(PipelineTimerContextPtr context, std::shared_ptr<PipelineTimerTask> task) { |
| 84 | + _pipeline_timer_context = std::move(context); |
| 85 | + _global_rf_timer = std::move(task); |
| 86 | + } |
| 87 | +}; |
| 88 | + |
76 | 89 | class CountingObserver final : public PipelineObserver { |
77 | 90 | public: |
78 | 91 | void source_trigger() override { |
@@ -303,6 +316,52 @@ TEST_F(PipelineTimerTaskTest, pipeline_timer_context_submits_batched_rf_timeout_ |
303 | 316 | context.clear_rf_timeout_tasks(); |
304 | 317 | } |
305 | 318 |
|
| 319 | +// A queued or blocked driver abandoned when the driver executor is closed during shutdown is |
| 320 | +// destroyed without going through finalize(). The destructor must still unschedule the global |
| 321 | +// runtime-filter timer, otherwise the timer thread runs a task whose owning shared_ptr is gone and |
| 322 | +// doRun()'s shared_from_this() throws std::bad_weak_ptr. |
| 323 | +TEST_F(PipelineTimerTaskTest, pipeline_driver_destructor_unschedules_global_rf_timer) { |
| 324 | + auto context = std::make_shared<PipelineTimerContext>(&timer); |
| 325 | + auto task = std::make_shared<ProbeTask>(); |
| 326 | + ASSERT_OK(context->schedule(task.get(), future_abstime(3600))); |
| 327 | + ASSERT_NE(0, task->tid()); |
| 328 | + |
| 329 | + auto driver = std::make_unique<TimerTestPipelineDriver>(); |
| 330 | + driver->register_global_rf_timer(context, task); |
| 331 | + |
| 332 | + // Intentionally skip finalize(). |
| 333 | + driver.reset(); |
| 334 | + |
| 335 | + // The destructor must already have removed the task; a second unschedule finds nothing. |
| 336 | + EXPECT_EQ(-1, timer.unschedule(task.get())); |
| 337 | +} |
| 338 | + |
| 339 | +// The destructor must block until an already-running global RF timer task returns, mirroring |
| 340 | +// finalize()'s unschedule_and_join, so the task is never freed while doRun() is still executing. |
| 341 | +TEST_F(PipelineTimerTaskTest, pipeline_driver_destructor_waits_for_running_global_rf_timer) { |
| 342 | + auto context = std::make_shared<PipelineTimerContext>(&timer); |
| 343 | + auto task = std::make_shared<ProbeTask>(); |
| 344 | + task->block_until_released = true; |
| 345 | + ASSERT_OK(context->schedule(task.get(), past_abstime())); |
| 346 | + task->run_enter.acquire(); |
| 347 | + |
| 348 | + auto driver = std::make_unique<TimerTestPipelineDriver>(); |
| 349 | + driver->register_global_rf_timer(context, task); |
| 350 | + |
| 351 | + std::atomic<bool> destructor_returned{false}; |
| 352 | + std::thread destructor([driver = std::move(driver), &destructor_returned]() mutable { |
| 353 | + driver.reset(); |
| 354 | + destructor_returned.store(true, std::memory_order_release); |
| 355 | + }); |
| 356 | + |
| 357 | + std::this_thread::sleep_for(std::chrono::milliseconds(50)); |
| 358 | + EXPECT_FALSE(destructor_returned.load(std::memory_order_acquire)); |
| 359 | + |
| 360 | + task->run_gate.release(); |
| 361 | + destructor.join(); |
| 362 | + EXPECT_TRUE(destructor_returned.load(std::memory_order_acquire)); |
| 363 | +} |
| 364 | + |
306 | 365 | // LightTimerTask goes through a separate schedule/unschedule pair and lacks the |
307 | 366 | // _finished / _has_consumer protocol. Cover it to prevent regressions in the |
308 | 367 | // other overload of PipelineTimer. |
|
0 commit comments