mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
chore: use condition variable to shutdown websocket pools (#5721)
This commit is contained in:
parent
19f449866e
commit
1c827f6288
9 changed files with 237 additions and 30 deletions
|
@ -131,6 +131,7 @@
|
||||||
- Dev: Unified parsing of historic and live IRC messages. (#5678)
|
- Dev: Unified parsing of historic and live IRC messages. (#5678)
|
||||||
- Dev: 7TV's `entitlement.reset` is now explicitly ignored. (#5685)
|
- Dev: 7TV's `entitlement.reset` is now explicitly ignored. (#5685)
|
||||||
- Dev: Qt 6.8 and later now default to the GDI fontengine. (#5710)
|
- Dev: Qt 6.8 and later now default to the GDI fontengine. (#5710)
|
||||||
|
- Dev: Moved to condition variables when shutting down worker threads. (#5721)
|
||||||
|
|
||||||
## 2.5.1
|
## 2.5.1
|
||||||
|
|
||||||
|
|
|
@ -514,6 +514,8 @@ set(SOURCE_FILES
|
||||||
util/LayoutHelper.hpp
|
util/LayoutHelper.hpp
|
||||||
util/LoadPixmap.cpp
|
util/LoadPixmap.cpp
|
||||||
util/LoadPixmap.hpp
|
util/LoadPixmap.hpp
|
||||||
|
util/OnceFlag.cpp
|
||||||
|
util/OnceFlag.hpp
|
||||||
util/RapidjsonHelpers.cpp
|
util/RapidjsonHelpers.cpp
|
||||||
util/RapidjsonHelpers.hpp
|
util/RapidjsonHelpers.hpp
|
||||||
util/RatelimitBucket.cpp
|
util/RatelimitBucket.cpp
|
||||||
|
|
|
@ -8,10 +8,12 @@
|
||||||
#include "providers/twitch/PubSubHelpers.hpp"
|
#include "providers/twitch/PubSubHelpers.hpp"
|
||||||
#include "util/DebugCount.hpp"
|
#include "util/DebugCount.hpp"
|
||||||
#include "util/ExponentialBackoff.hpp"
|
#include "util/ExponentialBackoff.hpp"
|
||||||
|
#include "util/OnceFlag.hpp"
|
||||||
#include "util/RenameThread.hpp"
|
#include "util/RenameThread.hpp"
|
||||||
|
|
||||||
#include <pajlada/signals/signal.hpp>
|
#include <pajlada/signals/signal.hpp>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QScopeGuard>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringBuilder>
|
#include <QStringBuilder>
|
||||||
#include <websocketpp/client.hpp>
|
#include <websocketpp/client.hpp>
|
||||||
|
@ -120,6 +122,11 @@ public:
|
||||||
this->work_ = std::make_shared<boost::asio::io_service::work>(
|
this->work_ = std::make_shared<boost::asio::io_service::work>(
|
||||||
this->websocketClient_.get_io_service());
|
this->websocketClient_.get_io_service());
|
||||||
this->mainThread_.reset(new std::thread([this] {
|
this->mainThread_.reset(new std::thread([this] {
|
||||||
|
// make sure we set in any case, even exceptions
|
||||||
|
auto guard = qScopeGuard([&] {
|
||||||
|
this->stoppedFlag_.set();
|
||||||
|
});
|
||||||
|
|
||||||
runThread();
|
runThread();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -142,22 +149,34 @@ public:
|
||||||
|
|
||||||
this->work_.reset();
|
this->work_.reset();
|
||||||
|
|
||||||
if (this->mainThread_->joinable())
|
if (!this->mainThread_->joinable())
|
||||||
{
|
{
|
||||||
// NOTE: We spawn a new thread to join the websocket thread.
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE:
|
||||||
// There is a case where a new client was initiated but not added to the clients list.
|
// There is a case where a new client was initiated but not added to the clients list.
|
||||||
// We just don't join the thread & let the operating system nuke the thread if joining fails
|
// We just don't join the thread & let the operating system nuke the thread if joining fails
|
||||||
// within 1s.
|
// within 1s.
|
||||||
auto joiner = std::async(std::launch::async, &std::thread::join,
|
if (this->stoppedFlag_.waitFor(std::chrono::seconds{1}))
|
||||||
this->mainThread_.get());
|
|
||||||
if (joiner.wait_for(std::chrono::seconds(1)) ==
|
|
||||||
std::future_status::timeout)
|
|
||||||
{
|
{
|
||||||
|
this->mainThread_->join();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
qCWarning(chatterinoLiveupdates)
|
qCWarning(chatterinoLiveupdates)
|
||||||
<< "Thread didn't join within 1 second, rip it out";
|
<< "Thread didn't finish within 1 second, force-stop the client";
|
||||||
this->websocketClient_.stop();
|
this->websocketClient_.stop();
|
||||||
|
if (this->stoppedFlag_.waitFor(std::chrono::milliseconds{100}))
|
||||||
|
{
|
||||||
|
this->mainThread_->join();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
qCWarning(chatterinoLiveupdates)
|
||||||
|
<< "Thread didn't finish after stopping, discard it";
|
||||||
|
// detach the thread so the destructor doesn't attempt any joining
|
||||||
|
this->mainThread_->detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -394,6 +413,7 @@ private:
|
||||||
|
|
||||||
liveupdates::WebsocketClient websocketClient_;
|
liveupdates::WebsocketClient websocketClient_;
|
||||||
std::unique_ptr<std::thread> mainThread_;
|
std::unique_ptr<std::thread> mainThread_;
|
||||||
|
OnceFlag stoppedFlag_;
|
||||||
|
|
||||||
const QString host_;
|
const QString host_;
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,10 @@
|
||||||
#include "util/RenameThread.hpp"
|
#include "util/RenameThread.hpp"
|
||||||
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
#include <QScopeGuard>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <future>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
@ -560,6 +560,11 @@ void PubSub::start()
|
||||||
this->work = std::make_shared<boost::asio::io_service::work>(
|
this->work = std::make_shared<boost::asio::io_service::work>(
|
||||||
this->websocketClient.get_io_service());
|
this->websocketClient.get_io_service());
|
||||||
this->thread = std::make_unique<std::thread>([this] {
|
this->thread = std::make_unique<std::thread>([this] {
|
||||||
|
// make sure we set in any case, even exceptions
|
||||||
|
auto guard = qScopeGuard([&] {
|
||||||
|
this->stoppedFlag_.set();
|
||||||
|
});
|
||||||
|
|
||||||
runThread();
|
runThread();
|
||||||
});
|
});
|
||||||
renameThread(*this->thread, "PubSub");
|
renameThread(*this->thread, "PubSub");
|
||||||
|
@ -578,23 +583,36 @@ void PubSub::stop()
|
||||||
|
|
||||||
this->work.reset();
|
this->work.reset();
|
||||||
|
|
||||||
if (this->thread->joinable())
|
if (!this->thread->joinable())
|
||||||
{
|
{
|
||||||
// NOTE: We spawn a new thread to join the websocket thread.
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE:
|
||||||
// There is a case where a new client was initiated but not added to the clients list.
|
// There is a case where a new client was initiated but not added to the clients list.
|
||||||
// We just don't join the thread & let the operating system nuke the thread if joining fails
|
// We just don't join the thread & let the operating system nuke the thread if joining fails
|
||||||
// within 1s.
|
// within 1s.
|
||||||
// We could fix the underlying bug, but this is easier & we realistically won't use this exact code
|
// We could fix the underlying bug, but this is easier & we realistically won't use this exact code
|
||||||
// for super much longer.
|
// for super much longer.
|
||||||
auto joiner = std::async(std::launch::async, &std::thread::join,
|
if (this->stoppedFlag_.waitFor(std::chrono::seconds{1}))
|
||||||
this->thread.get());
|
|
||||||
if (joiner.wait_for(1s) == std::future_status::timeout)
|
|
||||||
{
|
{
|
||||||
qCWarning(chatterinoPubSub)
|
this->thread->join();
|
||||||
<< "Thread didn't join within 1 second, rip it out";
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCWarning(chatterinoLiveupdates)
|
||||||
|
<< "Thread didn't finish within 1 second, force-stop the client";
|
||||||
this->websocketClient.stop();
|
this->websocketClient.stop();
|
||||||
|
if (this->stoppedFlag_.waitFor(std::chrono::milliseconds{100}))
|
||||||
|
{
|
||||||
|
this->thread->join();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
qCWarning(chatterinoLiveupdates)
|
||||||
|
<< "Thread didn't finish after stopping, discard it";
|
||||||
|
// detach the thread so the destructor doesn't attempt any joining
|
||||||
|
this->thread->detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PubSub::listenToWhispers()
|
bool PubSub::listenToWhispers()
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include "providers/twitch/PubSubClientOptions.hpp"
|
#include "providers/twitch/PubSubClientOptions.hpp"
|
||||||
#include "providers/twitch/PubSubWebsocket.hpp"
|
#include "providers/twitch/PubSubWebsocket.hpp"
|
||||||
#include "util/ExponentialBackoff.hpp"
|
#include "util/ExponentialBackoff.hpp"
|
||||||
|
#include "util/OnceFlag.hpp"
|
||||||
|
|
||||||
#include <boost/asio/io_service.hpp>
|
#include <boost/asio/io_service.hpp>
|
||||||
#include <boost/asio/ssl/context.hpp>
|
#include <boost/asio/ssl/context.hpp>
|
||||||
|
@ -267,6 +268,8 @@ private:
|
||||||
const QString host_;
|
const QString host_;
|
||||||
const PubSubClientOptions clientOptions_;
|
const PubSubClientOptions clientOptions_;
|
||||||
|
|
||||||
|
OnceFlag stoppedFlag_;
|
||||||
|
|
||||||
bool stopping_{false};
|
bool stopping_{false};
|
||||||
|
|
||||||
#ifdef FRIEND_TEST
|
#ifdef FRIEND_TEST
|
||||||
|
|
33
src/util/OnceFlag.cpp
Normal file
33
src/util/OnceFlag.cpp
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#include "util/OnceFlag.hpp"
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
OnceFlag::OnceFlag() = default;
|
||||||
|
OnceFlag::~OnceFlag() = default;
|
||||||
|
|
||||||
|
void OnceFlag::set()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::unique_lock guard(this->mutex);
|
||||||
|
this->flag = true;
|
||||||
|
}
|
||||||
|
this->condvar.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OnceFlag::waitFor(std::chrono::milliseconds ms)
|
||||||
|
{
|
||||||
|
std::unique_lock lock(this->mutex);
|
||||||
|
return this->condvar.wait_for(lock, ms, [this] {
|
||||||
|
return this->flag;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnceFlag::wait()
|
||||||
|
{
|
||||||
|
std::unique_lock lock(this->mutex);
|
||||||
|
this->condvar.wait(lock, [this] {
|
||||||
|
return this->flag;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
41
src/util/OnceFlag.hpp
Normal file
41
src/util/OnceFlag.hpp
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
/// @brief A flag that can only be set once which notifies waiters.
|
||||||
|
///
|
||||||
|
/// This can be used to synchronize with other threads. Note that waiting
|
||||||
|
/// threads will be suspended.
|
||||||
|
class OnceFlag
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OnceFlag();
|
||||||
|
~OnceFlag();
|
||||||
|
|
||||||
|
/// Set this flag and notify waiters
|
||||||
|
void set();
|
||||||
|
|
||||||
|
/// @brief Wait for at most `ms` until this flag is set.
|
||||||
|
///
|
||||||
|
/// The calling thread will be suspended during the wait.
|
||||||
|
///
|
||||||
|
/// @param ms The maximum time to wait for this flag
|
||||||
|
/// @returns `true` if this flag was set during the wait or before
|
||||||
|
bool waitFor(std::chrono::milliseconds ms);
|
||||||
|
|
||||||
|
/// @brief Wait until this flag is set by another thread
|
||||||
|
///
|
||||||
|
/// The calling thread will be suspended during the wait.
|
||||||
|
void wait();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable condvar;
|
||||||
|
bool flag = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
|
@ -51,6 +51,7 @@ set(test_SOURCES
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/Plugins.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/Plugins.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/TwitchIrc.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/TwitchIrc.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/IgnoreController.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/IgnoreController.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/OnceFlag.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.hpp
|
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.hpp
|
||||||
# Add your new file above this line!
|
# Add your new file above this line!
|
||||||
|
|
88
tests/src/OnceFlag.cpp
Normal file
88
tests/src/OnceFlag.cpp
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
#include "util/OnceFlag.hpp"
|
||||||
|
|
||||||
|
#include "Test.hpp"
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
// this test shouldn't time out (no assert necessary)
|
||||||
|
TEST(OnceFlag, basic)
|
||||||
|
{
|
||||||
|
OnceFlag startedFlag;
|
||||||
|
OnceFlag startedAckFlag;
|
||||||
|
OnceFlag stoppedFlag;
|
||||||
|
|
||||||
|
std::thread t([&] {
|
||||||
|
startedFlag.set();
|
||||||
|
startedAckFlag.wait();
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds{50});
|
||||||
|
stoppedFlag.set();
|
||||||
|
});
|
||||||
|
|
||||||
|
startedFlag.wait();
|
||||||
|
startedAckFlag.set();
|
||||||
|
stoppedFlag.wait();
|
||||||
|
|
||||||
|
t.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(OnceFlag, waitFor)
|
||||||
|
{
|
||||||
|
OnceFlag startedFlag;
|
||||||
|
OnceFlag startedAckFlag;
|
||||||
|
OnceFlag stoppedFlag;
|
||||||
|
|
||||||
|
std::thread t([&] {
|
||||||
|
startedFlag.set();
|
||||||
|
startedAckFlag.wait();
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds{100});
|
||||||
|
stoppedFlag.set();
|
||||||
|
});
|
||||||
|
|
||||||
|
startedFlag.wait();
|
||||||
|
startedAckFlag.set();
|
||||||
|
|
||||||
|
auto start = std::chrono::system_clock::now();
|
||||||
|
ASSERT_TRUE(stoppedFlag.waitFor(std::chrono::milliseconds{200}));
|
||||||
|
auto stop = std::chrono::system_clock::now();
|
||||||
|
|
||||||
|
ASSERT_LT(stop - start, std::chrono::milliseconds{150});
|
||||||
|
|
||||||
|
start = std::chrono::system_clock::now();
|
||||||
|
ASSERT_TRUE(stoppedFlag.waitFor(std::chrono::milliseconds{1000}));
|
||||||
|
stop = std::chrono::system_clock::now();
|
||||||
|
|
||||||
|
ASSERT_LT(stop - start, std::chrono::milliseconds{10});
|
||||||
|
|
||||||
|
start = std::chrono::system_clock::now();
|
||||||
|
stoppedFlag.wait();
|
||||||
|
stop = std::chrono::system_clock::now();
|
||||||
|
|
||||||
|
ASSERT_LT(stop - start, std::chrono::milliseconds{10});
|
||||||
|
|
||||||
|
t.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(OnceFlag, waitForTimeout)
|
||||||
|
{
|
||||||
|
OnceFlag startedFlag;
|
||||||
|
OnceFlag startedAckFlag;
|
||||||
|
OnceFlag stoppedFlag;
|
||||||
|
|
||||||
|
std::thread t([&] {
|
||||||
|
startedFlag.set();
|
||||||
|
startedAckFlag.wait();
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds{100});
|
||||||
|
stoppedFlag.set();
|
||||||
|
});
|
||||||
|
|
||||||
|
startedFlag.wait();
|
||||||
|
startedAckFlag.set();
|
||||||
|
|
||||||
|
ASSERT_FALSE(stoppedFlag.waitFor(std::chrono::milliseconds{25}));
|
||||||
|
stoppedFlag.wait();
|
||||||
|
|
||||||
|
t.join();
|
||||||
|
}
|
Loading…
Reference in a new issue