Skip to content

Conversation

@vladstepanyuk
Copy link
Collaborator

@vladstepanyuk vladstepanyuk commented Oct 30, 2025

VERIFY failed (2025-10-30T09:45:33.503210Z): 
  cloud/storage/core/libs/common/thread_pool.cpp:247
  ReleaseWorker(): requirement count > 1 failed
BackTrace(void**, unsigned long)+29 (0x10EB5FD)
FormatBackTrace(IOutputStream*)+32 (0x10EBAD0)
PrintBackTrace()+17 (0x10EBB21)
NPrivate::InternalPanicImpl(int, char const*, char const*, int, int, int, TBasicStringBuf<char, std::__y1::char_traits<char> >, char const*, unsigned long)+995 (0x114C713)
NPrivate::Panic(NPrivate::TStaticBuf const&, int, char const*, char const*, char const*, ...)+418 (0x113F8E2)
??+0 (0x1550736)
??+0 (0x155032C)
??+0 (0x155021D)
??+0 (0x114B216)
??+0 (0x1151AFD)
??+0 (0x7F43337B5609)
clone+67 (0x7F43336DA353)

Сейчас возможен следующий сценарий:
Пусть есть два треда T1, T2 и Thread Pool с одним воркером
изначально до запуска пула RunningWorkers равен 0

Y_CACHE_ALIGNED TAtomic RunningWorkers = 0;

  • тред T1 ставит таску на исполнение, увеличивает кол-во RunningWorkers до 1 внутри функции AllocateWorker и после этого (т.к. у него получилось увеличить счетчик) пытается разбудить воркеров
    void Enqueue(ITaskPtr task) override
    {
    Queue.Enqueue(std::move(task));
    if (AllocateWorker()) {
    WakeUpWorker();
    }
    }
  • тред T1 зависает внутри бесконечного цикла (т.к. нет спящих тредов, которые можно разбудить, из-за того что пулл еще не запущен)
    for (;;) {
    for (auto& worker: Workers) {
    if (WakeUp(worker)) {
    return;
    }
    }
    }
  • тред T2 стартует тред пулл сетит RunningWorkers
    void Start() override
    {
    AtomicSet(RunningWorkers, NumWorkers);
    for (auto& worker: Workers) {
    worker.Thread->Start();
    }
    }
  • тред ThreadPool worker успешно исполняет поставленную таску, уменьшает кол-во RunningWorkers до 0, и засыпает на спинлоке в функции Wait на 142 строчке
    void Run(TWorker& worker)
    {
    ::NCloud::SetCurrentThreadName(worker.Name);
    NProfiling::TMemoryTagScope tagScope(MemoryTagScope.c_str());
    while (AtomicGet(ShouldStop) == 0) {
    if (auto task = Queue.Dequeue()) {
    task->Execute();
    continue;
    }
    if (ReleaseWorker()) {
    Wait(worker);
    }
    }
    }
  • тред T1 будит ThreadPool worker'а
    ui32 state = AtomicSwap(&worker.State, TWorker::RUNNING);
  • тред ThreadPool worker пытается исполнить таску из осереди, вызывает опять функцию ReleaseWorker и крашится на верифайке, т.к. RunningWorkers равен 0
    void Run(TWorker& worker)
    {
    ::NCloud::SetCurrentThreadName(worker.Name);
    NProfiling::TMemoryTagScope tagScope(MemoryTagScope.c_str());
    while (AtomicGet(ShouldStop) == 0) {
    if (auto task = Queue.Dequeue()) {
    task->Execute();
    continue;
    }
    if (ReleaseWorker()) {
    Wait(worker);
    }
    }
    }

@vladstepanyuk vladstepanyuk added large-tests Launch large tests for PR disk_manager Add this label to run only cloud/disk_manager build and tests on PR blockstore Add this label to run only cloud/blockstore build and tests on PR filestore Add this label to run only cloud/filestore build and tests on PR labels Oct 30, 2025
@sharpeye
Copy link
Collaborator

В каких условиях сработала верифайка - в каких-то UT?

future.GetValueSync();

// Sleep to be sure that task will be enqueued before start.
Sleep(TDuration::Seconds(1));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А если вызвать promise.SetValue(); после threadPool->Execute ?

Можно взять latch для синхронизации:

auto threadPool = CreateThreadPool("thread", 1);

std::latch enqueued{1};

std::thread thread([&] {
    auto future = threadPool->Execute([] { return 42; });
    enqueued.count_down();

    UNIT_ASSERT_EQUAL(42, future.GetValue(WaitTimeout));
});

enqueued.wait();

threadPool->Start();
threadPool->Stop();

thread.join();

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

я хотел написать тест который будет крашиться в отсутствии фикса.
Если фикса нет, то threadPool->Execute зависнет пока не вызовется threadPool->Start() т.е. будет дедлок.
Как бы да зафейлится тест но хотелось бы краш
по хорошему надо было бы вставить count_down после AllocateWorker

void Enqueue(ITaskPtr task) override
{
Queue.Enqueue(std::move(task));
if (AllocateWorker()) {
WakeUpWorker();
}
}

но так сделать понятно не получится. Так что не понятно как воспроизвести стабильно краш без слипов и без того чтобы лезть в кишки тред пула

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enqueued.wait();
// Sleep to make it more likely that ReleaseWorker is called before starting the thread pool
Sleep(1s);
threadPool->Start();

@vladstepanyuk
Copy link
Collaborator Author

В каких условиях сработала верифайка - в каких-то UT?

Написал интеграционные тесты для фичи с открытием закрытием девайсов в диск агенте ( код тот же что и в пре #4299)
Конкретно крашился диск агент (точнее Нбс с диск агент актором)

komarevtsev-d
komarevtsev-d previously approved these changes Oct 30, 2025
, SpinCycles(DurationToCyclesSafe(SPIN_TIMEOUT))
, MemoryTagScope(std::move(memoryTagScope))
, Workers(numWorkers)
, RunningWorkers(NumWorkers)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Кажется теперь название не соответствует действительности. В конструкторе воркеры не запускаются. Ну и да, очень подозрительно выглядит сценарий когда мы начинаем юзать пул без вызова Start (у нас тогда любой IStartable по идее может страдать от такого же). Если кто-то так делает, то лучше наверное эту проблему решать в Execute (бросать исключение, ждать на фьюче до вызова старт), но в общем попытка использования threadpool без вызова Start выглядит как нарушение "контракта"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не понимаю, почему это нарушение контракта. Тот же шедулер (https://github.com/ydb-platform/nbs/blob/6cfc50c76085138d8908ffa4bdef8bb3ecb22f4a/cloud/blockstore/libs/daemon/common/bootstrap.cpp#L966C28-L966C37) стартует самым последним, и любая компонента может зашедулить таски в него до его старта, тред пул — очень похожая по смыслу штука. Тем более у нас в компонентах циклы по зависимостям, и как бэ наоборот надо стремиться к тому, чтобы все компоненты корректно себя вели, если их ручки дергают до старта, а не крашить процесс исключением, если такое происходит. Да и в целом есть 2 однострочных фикса (одинаковых по сложности): один из них расширяет допустимые сценарии использования объекта, убирает возможность случайно совершить ошибку, а второй опирается на какие-то совершенно неочевидные контракты(которые к одним компонентам приложимы, к другим нет) и фиксит только поведение внутри моего пра, при этом кто-то через N времени может заиспользовать случайно тред пул до его старта и получить исключение и, соответственно, краш. И ладно, если это произойдет при тестировании, такое может и до прода успеть доехать и крашнуть процесс там, я уже фиксил баги, которые стреляют только в проде, если какую-то из ручек дернули слишком рано на старте процесса, так что такой сценарий мне кажется довольно вероятным

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ну а насчет названия, оно и до этого не сильно соответствовало реальности. Тип этот атомик означает кол-во воркеров, которые не ждут задачки в функции wait, по сути. Можно, конечно, переименовать, но тогда уж лучше отдельным пром наверное, или изменить RunningWorkers на WaitingWorkers

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Откуда берётся сценарий использования трэд пула до его старта?
Такого не должно происходит, если происходит - нужно чинить вызывающий код

Плюс в Enqueue должна быть верифайка что thread pool в состоянии started

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Откуда берётся сценарий использования трэд пула до его старта?
Такого не должно происходит, если происходит - нужно чинить вызывающий код

Почему? У нас многие компоненты используются до старта. Тот же шедулер, который я уже упомянул.
Там прям целый отдельный комент что так задумано и что у нас есть циклы в зависимостях, а если есть циклы то есть и теоритическая вероятность, что кто-то что-то заиспользует до старта.

// we need to start scheduler after all other components for 2 reasons:
// 1) any component can schedule a task that uses a dependency that hasn't
// started yet
// 2) we have loops in our dependencies, so there is no 'correct' starting
// order
START_COMMON_COMPONENT(Scheduler);

помимо шедулера в акторную систему передаются RdmaClient, EndpointManager(через EndpointEventHandler)

START_KIKIMR_COMPONENT(ActorSystem);
START_COMMON_COMPONENT(EndpointManager);
START_COMMON_COMPONENT(Service);
START_COMMON_COMPONENT(VhostServer);
START_COMMON_COMPONENT(NbdServer);
START_COMMON_COMPONENT(GrpcEndpointListener);
START_COMMON_COMPONENT(Executor);
START_COMMON_COMPONENT(Server);
START_COMMON_COMPONENT(ServerStatsUpdater);
START_COMMON_COMPONENT(BackgroundThreadPool);
START_COMMON_COMPONENT(RdmaClient);
START_COMMON_COMPONENT(GetTraceServiceClient());
START_COMMON_COMPONENT(RdmaRequestServer);
START_COMMON_COMPONENT(RdmaTarget);
START_COMMON_COMPONENT(CellManager);

которые стартуют позже акторной системы

CellManager стартует самым последним почти хоть и передается в другие компоненты которые стартуют раньше.

Особенно вот вообще неочевидно почему для шедулера это допустимо и нормально, а для тред пула это недопустимое поведение.

Тем более сделать так чтобы можно было использовать тред пул до старта легко, буквально перетащить одну строчку, так зачем добавлять какую-то верифайку, на которую можно наткнуться максимально неожиданным способом причем в проде.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тем более сделать так чтобы можно было использовать тред пул до старта легко, буквально перетащить одну строчку, так зачем добавлять какую-то верифайку, на которую можно наткнуться максимально неожиданным способом причем в проде.

А что с остановкой? Если тредпул используется после остановки - это явная ошибка.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А что с остановкой? Если тредпул используется после остановки - это явная ошибка.

Я тут в целом согласен, что это может привести к неожиданным эффектам, из-за того что задача не исполнится. То есть код, который постит что-то в остановленный тред пул, лучше не множить и подсветить разботчику что он делает что-то не то.
Поэтому кажется достаточно Y_DEBUG_ABORT_UNLESS в этом месте. В проде взрываться смысла не вижу, т.к. критичность у таких багов низкая

Copy link
Collaborator

@SvartMetal SvartMetal Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Поговорили голосом. Перемещение RunningWorkers в конструктор, формально, не вносит неконсистентность в код, а даже исправляет её, потому что в конструкторе инициализируются Workers в стейте RUNNING, поэтому логично инициализировать RunningWorkers == Workers.size() == NumWorkers.

При этом остаётся формальная неконсистентность потому что после запуска конструктора воркеры не совсем running, ибо требуется вызов Start чтобы функция воркера начала выполнение. Но это минор

@github-actions
Copy link
Contributor

github-actions bot commented Oct 30, 2025

Note

This is an automated comment that will be appended during run.

🔴 linux-x86_64-relwithdebinfo: some tests FAILED for commit dac2122.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
9850 9848 0 1 0 1 0

🟢 linux-x86_64-relwithdebinfo: all tests PASSED for commit dac2122.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
2 2 0 0 0 0 0

@github-actions
Copy link
Contributor

github-actions bot commented Oct 31, 2025

Note

This is an automated comment that will be appended during run.

🔴 linux-x86_64-relwithdebinfo: some tests FAILED for commit e4faf1a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
9850 9848 0 1 0 1 0

🟢 linux-x86_64-relwithdebinfo: all tests PASSED for commit e4faf1a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
2 2 0 0 0 0 0

@vladstepanyuk vladstepanyuk added asan Launch builds with address sanitizer along with regular build tsan Launch builds with thread sanitizer along with regular build msan Launch builds with memory sanitizer along with regular build ubsan Launch builds with undefined behaviour sanitizer along with regular build labels Oct 31, 2025
@github-actions
Copy link
Contributor

Note

This is an automated comment that will be appended during run.

🟢 linux-x86_64-release-msan: all tests PASSED for commit e4faf1a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
7711 7711 0 0 0 0 0

@github-actions
Copy link
Contributor

Note

This is an automated comment that will be appended during run.

🟢 linux-x86_64-release-asan: all tests PASSED for commit e4faf1a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
7729 7729 0 0 0 0 0

@github-actions
Copy link
Contributor

Note

This is an automated comment that will be appended during run.

🟢 linux-x86_64-release-ubsan: all tests PASSED for commit e4faf1a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
7763 7763 0 0 0 0 0

@github-actions
Copy link
Contributor

Note

This is an automated comment that will be appended during run.

🔴 linux-x86_64-release-tsan: some tests FAILED for commit e4faf1a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
7717 7709 0 4 0 4 0

@github-actions
Copy link
Contributor

github-actions bot commented Oct 31, 2025

Note

This is an automated comment that will be appended during run.

🔴 linux-x86_64-relwithdebinfo: some tests FAILED for commit e4faf1a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
9850 9847 0 2 0 1 0

🟢 linux-x86_64-relwithdebinfo: all tests PASSED for commit e4faf1a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
4 4 0 0 0 0 0

@github-actions
Copy link
Contributor

github-actions bot commented Nov 5, 2025

Note

This is an automated comment that will be appended during run.

🟢 linux-x86_64-release-msan: all tests PASSED for commit 1168b9a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
7711 7711 0 0 0 0 0

@github-actions
Copy link
Contributor

github-actions bot commented Nov 5, 2025

Note

This is an automated comment that will be appended during run.

🟢 linux-x86_64-release-asan: all tests PASSED for commit 1168b9a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
7729 7729 0 0 0 0 0

@github-actions
Copy link
Contributor

github-actions bot commented Nov 5, 2025

Note

This is an automated comment that will be appended during run.

🟢 linux-x86_64-release-ubsan: all tests PASSED for commit 1168b9a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
7763 7763 0 0 0 0 0

@github-actions
Copy link
Contributor

github-actions bot commented Nov 5, 2025

Note

This is an automated comment that will be appended during run.

🔴 linux-x86_64-release-tsan: some tests FAILED for commit 1168b9a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
7717 7707 0 6 0 4 0

@github-actions
Copy link
Contributor

github-actions bot commented Nov 5, 2025

Note

This is an automated comment that will be appended during run.

🟢 linux-x86_64-relwithdebinfo: all tests PASSED for commit 1168b9a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
9840 9838 0 0 0 1 1

🟢 linux-x86_64-relwithdebinfo: all tests PASSED for commit 1168b9a.

TESTS PASSED ERRORS FAILED FAILED BUILD SKIPPED MUTED?
114 114 0 0 0 0 0


void Enqueue(ITaskPtr task) override
{
Y_ABORT_UNLESS(AtomicGet(ShouldStop) == 0);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Почему не DEBUG? Зачем взрываться в проде на этом?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

asan Launch builds with address sanitizer along with regular build blockstore Add this label to run only cloud/blockstore build and tests on PR disk_manager Add this label to run only cloud/disk_manager build and tests on PR filestore Add this label to run only cloud/filestore build and tests on PR large-tests Launch large tests for PR msan Launch builds with memory sanitizer along with regular build tsan Launch builds with thread sanitizer along with regular build ubsan Launch builds with undefined behaviour sanitizer along with regular build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants