4138fc7622 mistakenly removed an early
exit from non-main worker threads. this led to exceptions not ending
`process()` calls in a timely manner and instead draining the entire
work queue first, which for e.g. Interrupted errors would cause many
duplicated reports per worker thread instead of only one per thread.
it also did not properly rethrow a work item exception in all cases,
e.g. when all work had completed by the time `process()` was called.
due to a mistake in the thread starting check it was possible that a
system would require n² work items to start n worker threads if some
work items process quickly enough while other items block for a bit.
Change-Id: I7d59483580cda1c19980f9296074216a0fbf2c4e
112 lines
2.3 KiB
C++
112 lines
2.3 KiB
C++
#include "lix/libutil/thread-pool.hh"
|
|
#include <atomic>
|
|
#include <exception>
|
|
#include <gtest/gtest.h>
|
|
#include <memory>
|
|
|
|
static auto onThreadExit(auto fn)
|
|
{
|
|
auto deferred = kj::defer(std::move(fn));
|
|
return std::make_shared<decltype(deferred)>(std::move(deferred));
|
|
}
|
|
|
|
namespace nix {
|
|
|
|
TEST(ThreadPool, creates_threads)
|
|
{
|
|
ThreadPool t{"test", 2};
|
|
|
|
std::atomic_bool unblockA{false}, unblockB{false};
|
|
std::atomic_bool started{false};
|
|
|
|
t.enqueue([&] {
|
|
started = true;
|
|
started.notify_all();
|
|
unblockA.wait(false);
|
|
unblockB = true;
|
|
unblockB.notify_all();
|
|
});
|
|
started.wait(false);
|
|
|
|
// now no work is pending. the next enqueue should start a
|
|
// new thread; if it does not we'll deadlock and time out.
|
|
|
|
started = false;
|
|
t.enqueue([&] {
|
|
started = true;
|
|
started.notify_all();
|
|
unblockB.wait(false);
|
|
});
|
|
started.wait(false);
|
|
|
|
unblockA = true;
|
|
unblockA.notify_all();
|
|
|
|
t.process();
|
|
}
|
|
|
|
TEST(ThreadPool, early_quit)
|
|
{
|
|
ThreadPool t{"test", 2};
|
|
bool ran_anyway = false;
|
|
|
|
struct Dead : std::exception {};
|
|
|
|
std::atomic_bool unblockA{false}, unblockB{false};
|
|
std::atomic_bool started{false};
|
|
|
|
t.enqueue([&] {
|
|
started = true;
|
|
started.notify_all();
|
|
unblockA.wait(false);
|
|
thread_local auto _ = onThreadExit([&] {
|
|
unblockB = true;
|
|
unblockB.notify_all();
|
|
});
|
|
throw Dead{};
|
|
});
|
|
started.wait(false);
|
|
|
|
started = false;
|
|
t.enqueue([&] {
|
|
started = true;
|
|
started.notify_all();
|
|
unblockB.wait(false);
|
|
});
|
|
started.wait(false);
|
|
|
|
// this one should never run. the first thread saw an exception,
|
|
// and the second thread should have exited early because of it.
|
|
t.enqueue([&] { ran_anyway = true; });
|
|
|
|
unblockA = true;
|
|
unblockA.notify_all();
|
|
|
|
ASSERT_THROW(t.process(), Dead);
|
|
ASSERT_FALSE(ran_anyway);
|
|
}
|
|
|
|
TEST(ThreadPool, always_rethrows)
|
|
{
|
|
ThreadPool t{"test"};
|
|
|
|
struct Dead : std::exception {};
|
|
|
|
std::atomic_bool flag{false};
|
|
|
|
t.enqueue([&] {
|
|
thread_local auto _ = onThreadExit([&] {
|
|
flag = true;
|
|
flag.notify_all();
|
|
});
|
|
|
|
throw Dead{};
|
|
});
|
|
|
|
flag.wait(false);
|
|
|
|
ASSERT_THROW(t.process(), Dead);
|
|
}
|
|
|
|
}
|