Skip to content

Commit 8b432b7

Browse files
committed
cc engine: add tests for critical task cancellation
commit_hash:3ff5e3b9ee94fc59a4c232b6d26a392288f02c9e
1 parent 063166d commit 8b432b7

1 file changed

Lines changed: 61 additions & 0 deletions

File tree

core/src/engine/task/cancel_test.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
#include <userver/engine/deadline.hpp>
1010
#include <userver/engine/future.hpp>
1111
#include <userver/engine/single_consumer_event.hpp>
12+
#include <userver/engine/single_use_event.hpp>
1213
#include <userver/engine/sleep.hpp>
1314
#include <userver/engine/task/task_with_result.hpp>
1415
#include <userver/utest/utest.hpp>
1516
#include <userver/utils/async.hpp>
17+
#include <userver/utils/fast_scope_guard.hpp>
1618

1719
using namespace std::chrono_literals;
1820

@@ -435,4 +437,63 @@ UTEST(Cancel, ParentCancelledSample) {
435437
EXPECT_FALSE(deadline.IsReached());
436438
}
437439

440+
UTEST(Async, CancellationBeforeStartNormal) {
441+
// The test relies on there only being a single TaskProcessor thread, otherwise there could be a race
442+
// between `RequestCancel()` and the concurrent execution of the child task.
443+
ASSERT_EQ(GetThreadCount(), 1);
444+
445+
bool captures_destroyed = false;
446+
utils::FastScopeGuard guard([&captures_destroyed]() noexcept {
447+
EXPECT_TRUE(engine::current_task::IsTaskProcessorThread());
448+
captures_destroyed = true;
449+
});
450+
451+
auto task = engine::AsyncNoSpan([guard = std::move(guard)] { ADD_FAILURE() << "The task body should not start"; });
452+
453+
EXPECT_FALSE(task.IsFinished());
454+
EXPECT_FALSE(captures_destroyed);
455+
456+
// The task has not started by this point. This means that the task is cancelled before starting.
457+
// This should cause the task body to be skipped, because this is a non-Critical task.
458+
// See details on engine::TaskBase::State::kCancelled.
459+
task.RequestCancel();
460+
461+
task.WaitFor(utest::kMaxTestWaitTime);
462+
EXPECT_EQ(task.GetState(), engine::TaskBase::State::kCancelled);
463+
UEXPECT_THROW(task.Get(), engine::TaskCancelledException);
464+
// The task destructor should run even if the execution of the task body is skipped due to cancellation.
465+
EXPECT_TRUE(captures_destroyed);
466+
}
467+
468+
UTEST(Async, CancellationBeforeStartCritical) {
469+
// The test relies on there only being a single TaskProcessor thread, otherwise there could be a race
470+
// between `RequestCancel()` and the concurrent execution of the child task.
471+
ASSERT_EQ(GetThreadCount(), 1);
472+
473+
engine::SingleUseEvent event;
474+
auto task = engine::CriticalAsyncNoSpan([&event] {
475+
// The task should start (proven by `return true;` below and `Get() -> true`.)
476+
477+
EXPECT_TRUE(engine::current_task::impl::IsCritical());
478+
479+
// The task is still cancelled, and any waiting operation will be interrupted.
480+
EXPECT_TRUE(engine::current_task::IsCancelRequested());
481+
EXPECT_TRUE(engine::current_task::ShouldCancel());
482+
const auto deadline = engine::Deadline::FromDuration(utest::kMaxTestWaitTime);
483+
EXPECT_EQ(event.WaitUntil(deadline), engine::FutureStatus::kCancelled);
484+
485+
return true;
486+
});
487+
488+
EXPECT_FALSE(task.IsFinished());
489+
490+
// The task has not started by this point. This means that the task is cancelled before starting.
491+
// But because the task is started as Critical, the task body should be executed anyway.
492+
task.RequestCancel();
493+
494+
task.WaitFor(utest::kMaxTestWaitTime / 2);
495+
EXPECT_EQ(task.GetState(), engine::TaskBase::State::kCompleted);
496+
EXPECT_TRUE(task.Get());
497+
}
498+
438499
USERVER_NAMESPACE_END

0 commit comments

Comments
 (0)