|
22 | 22 | #include "programs/engine_testapp/mock_cookie.h" |
23 | 23 | #include "programs/engine_testapp/mock_server.h" |
24 | 24 | #include "tests/module_tests/test_helpers.h" |
| 25 | +#include "vb_visitors.h" |
25 | 26 | #include <executor/cb3_taskqueue.h> |
26 | 27 |
|
27 | 28 | #include <boost/algorithm/string/join.hpp> |
28 | 29 | #include <boost/filesystem.hpp> |
29 | 30 | #include <configuration_impl.h> |
| 31 | +#include <folly/synchronization/Baton.h> |
30 | 32 | #include <platform/dirutils.h> |
31 | 33 | #include <chrono> |
32 | 34 | #include <thread> |
@@ -108,10 +110,16 @@ void EventuallyPersistentEngineTest::TearDown() { |
108 | 110 | } |
109 | 111 |
|
110 | 112 | void EventuallyPersistentEngineTest::shutdownEngine() { |
111 | | - destroy_mock_cookie(cookie); |
112 | | - // Need to force the destroy (i.e. pass true) because |
113 | | - // NonIO threads may have been disabled (see DCPTest subclass). |
114 | | - engine->destroy(true); |
| 113 | + if (cookie) { |
| 114 | + destroy_mock_cookie(cookie); |
| 115 | + cookie = nullptr; |
| 116 | + } |
| 117 | + if (engine) { |
| 118 | + // Need to force the destroy (i.e. pass true) because |
| 119 | + // NonIO threads may have been disabled (see DCPTest subclass). |
| 120 | + engine->destroy(true); |
| 121 | + engine = nullptr; |
| 122 | + } |
115 | 123 | } |
116 | 124 |
|
117 | 125 | queued_item EventuallyPersistentEngineTest::store_item( |
@@ -495,3 +503,82 @@ INSTANTIATE_TEST_SUITE_P(EphemeralOrPersistent, |
495 | 503 | [](const ::testing::TestParamInfo<std::string>& info) { |
496 | 504 | return info.param; |
497 | 505 | }); |
| 506 | + |
| 507 | +/** |
| 508 | + * Regression test for MB-48925 - if a Task is scheduled against a Taskable |
| 509 | + * (Bucket) which has already been unregistered, then the ExecutorPool throws |
| 510 | + * and crashes the process. |
| 511 | + * Note: This test as it stands will *not* crash kv-engine if the fix for the |
| 512 | + * issue (see rest of this commit) is reverted. This is because the fix is |
| 513 | + * to change the currentVb Task member variable from an (owning) |
| 514 | + * shared_ptr<VBucket> to a (non-owning) VBucket* - the same thing TestVisior |
| 515 | + * below does. However it is included here for reference as to the original |
| 516 | + * problematic scenario. |
| 517 | + */ |
| 518 | +TEST_F(EventuallyPersistentEngineTest, MB48925_ScheduleTaskAfterUnregistered) { |
| 519 | + class TestVisitor : public InterruptableVBucketVisitor { |
| 520 | + public: |
| 521 | + TestVisitor(int& visitCount, |
| 522 | + folly::Baton<>& waitForVisit, |
| 523 | + folly::Baton<>& waitForDeinitialise) |
| 524 | + : visitCount(visitCount), |
| 525 | + waitForVisit(waitForVisit), |
| 526 | + waitForDeinitialise(waitForDeinitialise) { |
| 527 | + } |
| 528 | + |
| 529 | + void visitBucket(const VBucketPtr& vb) override { |
| 530 | + if (visitCount++ == 0) { |
| 531 | + currentVb = vb.get(); |
| 532 | + // On first call to visitBucket() perform the necessary |
| 533 | + // interleaved baton wait / sleeping. |
| 534 | + // Suspend execution of this thread; and allow main thread to |
| 535 | + // continue, delete Bucket and unregisterTaskable. |
| 536 | + waitForVisit.post(); |
| 537 | + |
| 538 | + // Keep task running until unregisterTaskable() has been called |
| 539 | + // and starts to cancel tasks - this ensures that the Task |
| 540 | + // object is still alive (ExecutorPool has a reference to it) |
| 541 | + // and hence is passed out from unregisterTaskable(), hence kept |
| 542 | + // alive past when KVBucket is deleted. |
| 543 | + waitForDeinitialise.wait(); |
| 544 | + } |
| 545 | + } |
| 546 | + InterruptableVBucketVisitor::ExecutionState shouldInterrupt() override { |
| 547 | + return ExecutionState::Continue; |
| 548 | + } |
| 549 | + |
| 550 | + int& visitCount; |
| 551 | + folly::Baton<>& waitForVisit; |
| 552 | + folly::Baton<>& waitForDeinitialise; |
| 553 | + |
| 554 | + // Model the behaviour of PagingVisitor prior to the bugfix. Note that |
| 555 | + // _if_ this is changed to a shared_ptr<VBucket> then we crash. |
| 556 | + VBucket* currentVb; |
| 557 | + }; |
| 558 | + |
| 559 | + int visitCount{0}; |
| 560 | + folly::Baton waitForVisit; |
| 561 | + folly::Baton waitForUnregister; |
| 562 | + engine->getKVBucket()->visitAsync( |
| 563 | + std::make_unique<TestVisitor>( |
| 564 | + visitCount, waitForVisit, waitForUnregister), |
| 565 | + "MB48925_ScheduleTaskAfterUnregistered", |
| 566 | + TaskId::ExpiredItemPagerVisitor, |
| 567 | + std::chrono::seconds{1}); |
| 568 | + waitForVisit.wait(); |
| 569 | + |
| 570 | + // Setup testing hook so we allow our TestVisitor's Task above to |
| 571 | + // continue once we are inside unregisterTaskable. |
| 572 | + ExecutorPool::get()->unregisterTaskablePostCancelHook = |
| 573 | + [&waitForUnregister]() { waitForUnregister.post(); }; |
| 574 | + |
| 575 | + // Delete the vbucket; so the file deletion will be performed by |
| 576 | + // VBucket::DeferredDeleter when the last reference goes out of scope |
| 577 | + // (expected to be the ExpiryPager. |
| 578 | + engine->getKVBucket()->deleteVBucket(vbid); |
| 579 | + |
| 580 | + // Destroy the engine. This does happen implicitly in TearDown, but call |
| 581 | + // it earlier because we need to call destroy() before our various Baton |
| 582 | + // local variables etc go out of scope. |
| 583 | + shutdownEngine(); |
| 584 | +} |
0 commit comments