Reworked http client to ease reuse

This commit is contained in:
emeric
2021-10-05 18:41:54 +02:00
parent 617139c2f4
commit 11f8a9e38b
16 changed files with 438 additions and 238 deletions
-1
View File
@@ -3,7 +3,6 @@ add_library(lmsscrobbling SHARED
impl/internal/InternalScrobbler.cpp impl/internal/InternalScrobbler.cpp
impl/listenbrainz/ListenBrainzScrobbler.cpp impl/listenbrainz/ListenBrainzScrobbler.cpp
impl/listenbrainz/ListensSynchronizer.cpp impl/listenbrainz/ListensSynchronizer.cpp
impl/listenbrainz/SendQueue.cpp
impl/listenbrainz/Utils.cpp impl/listenbrainz/Utils.cpp
impl/Scrobbling.cpp impl/Scrobbling.cpp
) )
@@ -32,6 +32,7 @@
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "utils/IConfig.hpp" #include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "Utils.hpp" #include "Utils.hpp"
@@ -142,10 +143,10 @@ namespace Scrobbling::ListenBrainz
Scrobbler::Scrobbler(boost::asio::io_context& ioContext, Database::Db& db) Scrobbler::Scrobbler(boost::asio::io_context& ioContext, Database::Db& db)
: _ioContext {ioContext} : _ioContext {ioContext}
, _db {db} , _db {db}
, _sendQueue {_ioContext, Service<IConfig>::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org")} , _baseAPIUrl {Service<IConfig>::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org")}
, _listensSynchronizer {_ioContext, db, _sendQueue} , _listensSynchronizer {_ioContext, db, _baseAPIUrl}
{ {
LOG(INFO) << "Starting ListenBrainz scrobbler... API endpoint = '" << _sendQueue.getAPIBaseURL(); LOG(INFO) << "Starting ListenBrainz scrobbler... API endpoint = '" << _baseAPIUrl;
} }
Scrobbler::~Scrobbler() Scrobbler::~Scrobbler()
@@ -187,53 +188,38 @@ namespace Scrobbling::ListenBrainz
void void
Scrobbler::enqueListen(const Listen& listen, const Wt::WDateTime& timePoint) Scrobbler::enqueListen(const Listen& listen, const Wt::WDateTime& timePoint)
{ {
std::optional<SendQueue::RequestData> requestData {createSubmitListenRequestData(listen, timePoint)}; Http::ClientPOSTRequestParameters request;
if (!requestData) request.url = _baseAPIUrl + "/1/submit-listens";
return;
SendQueue::Request submitListen {std::move(*requestData)};
if (timePoint.isValid()) if (timePoint.isValid())
{ {
submitListen.setPriority(SendQueue::Request::Priority::Normal); request.priority = Http::ClientRequestParameters::Priority::Normal;
submitListen.setOnSuccessFunc([=](std::string_view) request.onSuccessFunc = [=](std::string_view)
{ {
_listensSynchronizer.saveListen(TimedListen {listen, timePoint}); _listensSynchronizer.saveListen(TimedListen {listen, timePoint});
}); };
} }
else else
{ {
// We want "listen now" to appear as soon as possible // We want "listen now" to appear as soon as possible
submitListen.setPriority(SendQueue::Request::Priority::High); request.priority = Http::ClientRequestParameters::Priority::High;
} }
_sendQueue.enqueueRequest(std::move(submitListen)); std::string bodyText {listenToJsonString(_db.getTLSSession(), listen, timePoint, timePoint.isValid() ? "single" : "playing_now")};
}
std::optional<SendQueue::RequestData>
Scrobbler::createSubmitListenRequestData(const Listen& listen, const Wt::WDateTime& timePoint)
{
Database::Session& session {_db.getTLSSession()};
const std::optional<UUID> listenBrainzToken {Utils::getListenBrainzToken(session, listen.userId)};
if (!listenBrainzToken)
return std::nullopt;
SendQueue::RequestData requestData;
requestData.endpoint = "/1/submit-listens";
requestData.type = SendQueue::RequestData::Type::POST;
std::string bodyText {listenToJsonString(session, listen, timePoint, timePoint.isValid() ? "single" : "playing_now")};
if (bodyText.empty()) if (bodyText.empty())
{ {
LOG(DEBUG) << "Cannot convert listen to json: skipping"; LOG(DEBUG) << "Cannot convert listen to json: skipping";
return std::nullopt; return;
} }
requestData.message.addBodyText(bodyText); const std::optional<UUID> listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), listen.userId)};
requestData.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()}); if (!listenBrainzToken)
requestData.message.addHeader("Content-Type", "application/json"); return;
return requestData; request.message.addBodyText(bodyText);
request.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()});
request.message.addHeader("Content-Type", "application/json");
Service<Http::IClient>::get()->sendPOSTRequest(std::move(request));
} }
} // namespace Scrobbling::ListenBrainz } // namespace Scrobbling::ListenBrainz
@@ -24,7 +24,6 @@
#include "IScrobbler.hpp" #include "IScrobbler.hpp"
#include "ListensSynchronizer.hpp" #include "ListensSynchronizer.hpp"
#include "SendQueue.hpp"
namespace Database namespace Database
{ {
@@ -54,11 +53,11 @@ namespace Scrobbling::ListenBrainz
// Submit listens // Submit listens
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint); void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
std::optional<SendQueue::RequestData> createSubmitListenRequestData(const Listen& listen, const Wt::WDateTime& timePoint); //std::optional<SendQueue::RequestData> createSubmitListenRequestData(const Listen& listen, const Wt::WDateTime& timePoint);
boost::asio::io_context& _ioContext; boost::asio::io_context& _ioContext;
Database::Db& _db; Database::Db& _db;
SendQueue _sendQueue; std::string _baseAPIUrl;
ListensSynchronizer _listensSynchronizer; ListensSynchronizer _listensSynchronizer;
}; };
} // Scrobbling::ListenBrainz } // Scrobbling::ListenBrainz
@@ -34,6 +34,7 @@
#include "database/User.hpp" #include "database/User.hpp"
#include "scrobbling/Exception.hpp" #include "scrobbling/Exception.hpp"
#include "utils/IConfig.hpp" #include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/Service.hpp" #include "utils/Service.hpp"
@@ -45,17 +46,6 @@ namespace
{ {
using namespace Scrobbling::ListenBrainz; using namespace Scrobbling::ListenBrainz;
SendQueue::RequestData
createValidateTokenRequestData(std::string_view authToken)
{
SendQueue::RequestData requestData;
requestData.type = SendQueue::RequestData::Type::GET;
requestData.endpoint = "/1/validate-token";
requestData.headers = { {"Authorization", "Token " + std::string {authToken}} };
return requestData;
}
std::string std::string
parseValidateToken(std::string_view msgBody) parseValidateToken(std::string_view msgBody)
{ {
@@ -79,18 +69,6 @@ namespace
return listenBrainzUserName; return listenBrainzUserName;
} }
SendQueue::RequestData
createListenCountRequestData(std::string_view listenBrainzUserName)
{
LOG(DEBUG) << "Getting listen count for listenbrainz user '" << listenBrainzUserName << "'";
SendQueue::RequestData requestData;
requestData.type = SendQueue::RequestData::Type::GET;
requestData.endpoint = "/1/user/" + std::string {listenBrainzUserName} + "/listen-count";
return requestData;
}
std::optional<std::size_t> std::optional<std::size_t>
parseListenCount(std::string_view msgBody) parseListenCount(std::string_view msgBody)
{ {
@@ -109,18 +87,6 @@ namespace
} }
} }
SendQueue::RequestData
createGetListensRequestData(std::string_view listenBrainzUserName, const Wt::WDateTime& maxDateTime)
{
LOG(DEBUG) << "Getting listens for listenbrainz user '" << listenBrainzUserName << "' with max_ts = " << maxDateTime.toString();
SendQueue::RequestData requestData;
requestData.type = SendQueue::RequestData::Type::GET;
requestData.endpoint = "/1/user/" + std::string {listenBrainzUserName} + "/listens?max_ts=" + std::to_string(maxDateTime.toTime_t());
return requestData;
}
Database::Track::pointer Database::Track::pointer
tryMatchListen(Database::Session& session, const Wt::Json::Object& metadata) tryMatchListen(Database::Session& session, const Wt::Json::Object& metadata)
{ {
@@ -247,10 +213,10 @@ namespace
namespace Scrobbling::ListenBrainz namespace Scrobbling::ListenBrainz
{ {
ListensSynchronizer::ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, SendQueue& sendQueue) ListensSynchronizer::ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, std::string_view baseAPIUrl)
: _ioContext {ioContext} : _ioContext {ioContext}
, _db {db} , _db {db}
, _sendQueue {sendQueue} , _baseAPIUrl {baseAPIUrl}
, _maxSyncListenCount {Service<IConfig>::get()->getULong("listenbrainz-max-sync-listen-count", 1000)} , _maxSyncListenCount {Service<IConfig>::get()->getULong("listenbrainz-max-sync-listen-count", 1000)}
, _syncListensPeriod {Service<IConfig>::get()->getULong("listenbrainz-sync-listens-period-hours", 1)} , _syncListensPeriod {Service<IConfig>::get()->getULong("listenbrainz-sync-listens-period-hours", 1)}
{ {
@@ -386,31 +352,33 @@ namespace Scrobbling::ListenBrainz
{ {
assert(context.listenBrainzUserName.empty()); assert(context.listenBrainzUserName.empty());
std::optional<SendQueue::RequestData> requestData {createValidateTokenRequestData(context.userId)}; const std::optional<UUID> listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), context.userId)};
if (!requestData) if (!listenBrainzToken)
{ {
onGetListensEnded(context); onGetListensEnded(context);
return; return;
} }
SendQueue::Request validateTokenRequest {std::move(*requestData)}; Http::ClientGETRequestParameters request;
validateTokenRequest.setOnSuccessFunc([this, &context] (std::string_view msgBody) request.priority = Http::ClientRequestParameters::Priority::Low;
{ request.url = _baseAPIUrl + "/1/validate-token";
context.listenBrainzUserName = parseValidateToken(msgBody); request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} };
if (context.listenBrainzUserName.empty()) request.onSuccessFunc = [this, &context] (std::string_view msgBody)
{
context.listenBrainzUserName = parseValidateToken(msgBody);
if (context.listenBrainzUserName.empty())
{
onGetListensEnded(context);
return;
}
enqueGetListenCount(context);
};
request.onFailureFunc = [this, &context]
{ {
onGetListensEnded(context); onGetListensEnded(context);
return; };
}
enqueGetListenCount(context);
});
validateTokenRequest.setOnFailureFunc([this, &context]
{
onGetListensEnded(context);
});
validateTokenRequest.setPriority(SendQueue::Request::Priority::Low); Service<Http::IClient>::get()->sendGETRequest(std::move(request));
_sendQueue.enqueueRequest(std::move(validateTokenRequest));
} }
void void
@@ -418,32 +386,33 @@ namespace Scrobbling::ListenBrainz
{ {
assert(!context.listenBrainzUserName.empty()); assert(!context.listenBrainzUserName.empty());
SendQueue::Request getListenCountRequest {createListenCountRequestData(context.listenBrainzUserName)}; Http::ClientGETRequestParameters request;
getListenCountRequest.setOnSuccessFunc([=, &context] (std::string_view msgBody) request.url = _baseAPIUrl + "/1/user/" + std::string {context.listenBrainzUserName} + "/listen-count";
{ request.priority = Http::ClientRequestParameters::Priority::Low;
const auto listenCount = parseListenCount(msgBody); request.onSuccessFunc = [=, &context] (std::string_view msgBody)
if (listenCount) {
LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount; const auto listenCount = parseListenCount(msgBody);
if (listenCount)
LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount;
bool needSync {listenCount && (!context.listenCount || *context.listenCount != *listenCount)}; bool needSync {listenCount && (!context.listenCount || *context.listenCount != *listenCount)};
context.listenCount = listenCount; context.listenCount = listenCount;
if (!needSync) if (!needSync)
{
onGetListensEnded(context);
return;
}
context.maxDateTime = Wt::WDateTime::currentDateTime();
enqueGetListens(context);
};
request.onFailureFunc = [this, &context]
{ {
onGetListensEnded(context); onGetListensEnded(context);
return; };
}
context.maxDateTime = Wt::WDateTime::currentDateTime(); Service<Http::IClient>::get()->sendGETRequest(std::move(request));
enqueGetListens(context);
});
getListenCountRequest.setOnFailureFunc([this, &context]
{
onGetListensEnded(context);
});
getListenCountRequest.setPriority(SendQueue::Request::Priority::Low);
_sendQueue.enqueueRequest(std::move(getListenCountRequest));
} }
void void
@@ -451,37 +420,26 @@ namespace Scrobbling::ListenBrainz
{ {
assert(!context.listenBrainzUserName.empty()); assert(!context.listenBrainzUserName.empty());
SendQueue::Request getListensRequest {::createGetListensRequestData(context.listenBrainzUserName, context.maxDateTime)}; Http::ClientGETRequestParameters request;
getListensRequest.setOnSuccessFunc([=, &context] (std::string_view msgBody) request.url = _baseAPIUrl + "/1/user/" + context.listenBrainzUserName + "/listens?max_ts=" + std::to_string(context.maxDateTime.toTime_t());
{ request.priority = Http::ClientRequestParameters::Priority::Low;
processGetListensResponse(msgBody, context); request.onSuccessFunc = [=, &context] (std::string_view msgBody)
if (context.fetchedListenCount >= _maxSyncListenCount || !context.maxDateTime.isValid()) {
processGetListensResponse(msgBody, context);
if (context.fetchedListenCount >= _maxSyncListenCount || !context.maxDateTime.isValid())
{
onGetListensEnded(context);
return;
}
enqueGetListens(context);
};
request.onFailureFunc = [=, &context]
{ {
onGetListensEnded(context); onGetListensEnded(context);
return; };
}
enqueGetListens(context); Service<Http::IClient>::get()->sendGETRequest(std::move(request));
});
getListensRequest.setOnFailureFunc([=, &context]
{
onGetListensEnded(context);
});
getListensRequest.setPriority(SendQueue::Request::Priority::Low);
_sendQueue.enqueueRequest(std::move(getListensRequest));
}
std::optional<SendQueue::RequestData>
ListensSynchronizer::createValidateTokenRequestData(Database::UserId userId)
{
Database::Session& session {_db.getTLSSession()};
const std::optional<UUID> listenBrainzToken {Utils::getListenBrainzToken(session, userId)};
if (!listenBrainzToken)
return std::nullopt;
return ::createValidateTokenRequestData(listenBrainzToken->getAsString());
} }
void void
@@ -28,7 +28,6 @@
#include "database/Types.hpp" #include "database/Types.hpp"
#include "scrobbling/Listen.hpp" #include "scrobbling/Listen.hpp"
#include "SendQueue.hpp"
namespace Database namespace Database
{ {
@@ -43,7 +42,7 @@ namespace Scrobbling::ListenBrainz
class ListensSynchronizer class ListensSynchronizer
{ {
public: public:
ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, SendQueue& sendQueue); ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, std::string_view baseAPIUrl);
void saveListen(const TimedListen& listen); void saveListen(const TimedListen& listen);
@@ -78,14 +77,12 @@ namespace Scrobbling::ListenBrainz
void enqueValidateToken(UserContext& context); void enqueValidateToken(UserContext& context);
void enqueGetListenCount(UserContext& context); void enqueGetListenCount(UserContext& context);
void enqueGetListens(UserContext& context); void enqueGetListens(UserContext& context);
std::optional<SendQueue::RequestData> createValidateTokenRequestData(Database::UserId userId);
std::optional<SendQueue::RequestData> createGetListensRequestData(std::string_view listenBrainzUserName, const Wt::WDateTime& maxDateTime);
void processGetListensResponse(std::string_view body, UserContext& context); void processGetListensResponse(std::string_view body, UserContext& context);
boost::asio::io_context& _ioContext; boost::asio::io_context& _ioContext;
boost::asio::io_context::strand _strand {_ioContext}; boost::asio::io_context::strand _strand {_ioContext};
Database::Db& _db; Database::Db& _db;
SendQueue& _sendQueue; std::string _baseAPIUrl;
boost::asio::steady_timer _getListensTimer {_ioContext}; boost::asio::steady_timer _getListensTimer {_ioContext};
std::unordered_map<Database::UserId, UserContext> _userContexts; std::unordered_map<Database::UserId, UserContext> _userContexts;
+2
View File
@@ -1,4 +1,6 @@
add_library(lmsutils SHARED add_library(lmsutils SHARED
impl/http/Client.cpp
impl/http/SendQueue.cpp
impl/ChildProcess.cpp impl/ChildProcess.cpp
impl/ChildProcessManager.cpp impl/ChildProcessManager.cpp
impl/Config.cpp impl/Config.cpp
+1
View File
@@ -31,6 +31,7 @@ const char* getModuleName(Module mod)
case Module::DB: return "DB"; case Module::DB: return "DB";
case Module::DBUPDATER: return "DB UPDATER"; case Module::DBUPDATER: return "DB UPDATER";
case Module::FEATURE: return "FEATURE"; case Module::FEATURE: return "FEATURE";
case Module::HTTP: return "HTTP";
case Module::MAIN: return "MAIN"; case Module::MAIN: return "MAIN";
case Module::METADATA: return "METADATA"; case Module::METADATA: return "METADATA";
case Module::REMOTE: return "REMOTE"; case Module::REMOTE: return "REMOTE";
+73
View File
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Client.hpp"
#include "utils/Exception.hpp"
namespace Http
{
std::unique_ptr<IClient>
createClient(boost::asio::io_context& ioContext)
{
return std::make_unique<Client>(ioContext);
}
void
Client::sendGETRequest(ClientGETRequestParameters&& GETParams)
{
SendQueue& sendQueue {getOrCreateSendQueue(GETParams.url)};
sendQueue.sendRequest(std::make_unique<ClientRequest>(std::move(GETParams)));
}
void
Client::sendPOSTRequest(ClientPOSTRequestParameters&& POSTParams)
{
SendQueue& sendQueue {getOrCreateSendQueue(POSTParams.url)};
sendQueue.sendRequest(std::make_unique<ClientRequest>(std::move(POSTParams)));
}
SendQueue&
Client::getOrCreateSendQueue(const std::string& url)
{
Wt::Http::Client::URL parsedURL;
if (!Wt::Http::Client::parseUrl(url, parsedURL))
throw LmsException {"Cannot parse URL '" + url + "'"};
{
std::shared_lock lock {_sendQueuesMutex};
if (auto it = _sendQueues.find(parsedURL.host); it != std::cend(_sendQueues))
return it->second;
}
{
std::unique_lock lock {_sendQueuesMutex};
if (auto it = _sendQueues.find(parsedURL.host); it != std::cend(_sendQueues))
return it->second;
auto [it, inserted] {_sendQueues.emplace(parsedURL.host, _ioContext)};
assert(inserted);
return it->second;
}
}
} // namespace Http
+47
View File
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <unordered_map>
#include <string>
#include <shared_mutex>
#include "utils/http/IClient.hpp"
#include "SendQueue.hpp"
namespace Http
{
class Client final : public IClient
{
public:
Client(boost::asio::io_context& ioContext) : _ioContext {ioContext} {}
private:
void sendGETRequest(ClientGETRequestParameters&& request) override;
void sendPOSTRequest(ClientPOSTRequestParameters&& request) override;
SendQueue& getOrCreateSendQueue(const std::string& host);
boost::asio::io_context& _ioContext;
std::shared_mutex _sendQueuesMutex;
std::unordered_map<std::string, SendQueue> _sendQueues;
};
} // namespace Http
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include <variant>
#include "utils/http/ClientRequestParameters.hpp"
namespace Http
{
class ClientRequest
{
public:
ClientRequest(ClientGETRequestParameters&& GETParams) : _parameters {std::move(GETParams)} {}
ClientRequest(ClientPOSTRequestParameters&& POSTParams) : _parameters {std::move(POSTParams)} {}
std::size_t retryCount {};
const ClientRequestParameters& getParameters() const
{
const ClientRequestParameters* res;
std::visit([&](const auto& parameters)
{
res = &static_cast<const ClientRequestParameters&>(parameters);
}, _parameters);
return *res;
}
enum class Type
{
GET,
POST
};
Type getType() const
{
if (std::holds_alternative<ClientGETRequestParameters>(_parameters))
return Type::GET;
else
return Type::POST;
}
const ClientGETRequestParameters& getGETParameters() const
{
return std::get<ClientGETRequestParameters>(_parameters);
}
const ClientPOSTRequestParameters& getPOSTParameters() const
{
return std::get<ClientPOSTRequestParameters>(_parameters);
}
private:
std::variant<ClientGETRequestParameters, ClientPOSTRequestParameters> _parameters;
};
}
@@ -19,9 +19,10 @@
#include "SendQueue.hpp" #include "SendQueue.hpp"
#include <boost/asio/dispatch.hpp>
#include <boost/asio/bind_executor.hpp> #include <boost/asio/bind_executor.hpp>
#include "scrobbling/Exception.hpp" #include "utils/Exception.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
@@ -57,11 +58,10 @@ namespace
} }
} }
namespace Scrobbling::ListenBrainz namespace Http
{ {
SendQueue::SendQueue(boost::asio::io_context& ioContext, std::string_view apiBaseURL) SendQueue::SendQueue(boost::asio::io_context& ioContext)
: _ioContext {ioContext} : _ioContext {ioContext}
, _apiBaseURL {apiBaseURL}
{ {
_client.done().connect([this](Wt::AsioWrapper::error_code ec, const Wt::Http::Message& msg) _client.done().connect([this](Wt::AsioWrapper::error_code ec, const Wt::Http::Message& msg)
{ {
@@ -78,11 +78,11 @@ namespace Scrobbling::ListenBrainz
} }
void void
SendQueue::enqueueRequest(Request request) SendQueue::sendRequest(std::unique_ptr<ClientRequest> request)
{ {
_strand.dispatch([this, request = std::move(request)]() boost::asio::dispatch(_strand, [this, request = std::move(request)]() mutable
{ {
_sendQueue[request._priority].emplace_back(std::move(request)); _sendQueue[request->getParameters().priority].emplace_back(std::move(request));
if (_state == State::Idle) if (_state == State::Idle)
sendNextQueuedRequest(); sendNextQueuedRequest();
@@ -93,16 +93,17 @@ namespace Scrobbling::ListenBrainz
SendQueue::sendNextQueuedRequest() SendQueue::sendNextQueuedRequest()
{ {
assert(_state == State::Idle); assert(_state == State::Idle);
assert(!_currentRequest);
for (auto& [prio, requests] : _sendQueue) for (auto& [prio, requests] : _sendQueue)
{ {
LOG(DEBUG) << "Processing prio " << static_cast<int>(prio) << ", request count = " << requests.size(); LOG(DEBUG) << "Processing prio " << static_cast<int>(prio) << ", request count = " << requests.size();
while (!requests.empty()) while (!requests.empty())
{ {
Request request {std::move(requests.front())}; std::unique_ptr<ClientRequest> request {std::move(requests.front())};
requests.pop_front(); requests.pop_front();
if (!sendRequest(request._requestData)) if (!sendRequest(*request))
continue; continue;
_state = State::Sending; _state = State::Sending;
@@ -113,20 +114,19 @@ namespace Scrobbling::ListenBrainz
} }
bool bool
SendQueue::sendRequest(const RequestData& requestData) SendQueue::sendRequest(const ClientRequest& request)
{ {
const std::string url {_apiBaseURL + requestData.endpoint}; LOG(DEBUG) << "Sending request to url '" << request.getParameters().url << "'";
LOG(DEBUG) << "Sending request type " << (requestData.type == RequestData::Type::GET ? "GET" : "POST") << " to url '" << url << "'"; bool res {};
switch (request.getType())
bool res{};
switch (requestData.type)
{ {
case RequestData::Type::GET: case ClientRequest::Type::GET:
res = _client.get(url, requestData.headers); res = _client.get(request.getParameters().url, request.getGETParameters().headers);
break; break;
case RequestData::Type::POST:
res = _client.post(url, requestData.message); case ClientRequest::Type::POST:
res = _client.post(request.getParameters().url, request.getPOSTParameters().message);
break; break;
} }
@@ -146,43 +146,43 @@ namespace Scrobbling::ListenBrainz
} }
assert(_currentRequest); assert(_currentRequest);
Request request {std::move(*_currentRequest)};
_state = State::Idle; _state = State::Idle;
LOG(DEBUG) << "Client done. status = " << msg.status(); LOG(DEBUG) << "Client done. status = " << msg.status();
if (ec) if (ec)
onClientDoneError(std::move(request), ec); onClientDoneError(std::move(_currentRequest), ec);
else else
onClientDoneSuccess(std::move(request), msg); onClientDoneSuccess(std::move(_currentRequest), msg);
} }
void void
SendQueue::onClientDoneError(Request request, Wt::AsioWrapper::error_code ec) SendQueue::onClientDoneError(std::unique_ptr<ClientRequest> request, Wt::AsioWrapper::error_code ec)
{ {
LOG(ERROR) << "Retry " << request._retryCount << ", client error: '" << ec.message() << "'"; LOG(ERROR) << "Retry " << request->retryCount << ", client error: '" << ec.message() << "'";
// may be a network error, try again later // may be a network error, try again later
throttle(_defaultRetryWaitDuration); throttle(_defaultRetryWaitDuration);
if (request._retryCount++ < _maxRetryCount) if (request->retryCount++ < _maxRetryCount)
{ {
_sendQueue[request._priority].emplace_front(std::move(request)); _sendQueue[request->getParameters().priority].emplace_front(std::move(request));
} }
else else
{ {
LOG(ERROR) << "Too many retries, giving up operation and throttle"; LOG(ERROR) << "Too many retries, giving up operation and throttle";
if (request._onFailureFunc) if (request->getParameters().onFailureFunc)
request._onFailureFunc(); request->getParameters().onFailureFunc();
} }
} }
void void
SendQueue::onClientDoneSuccess(Request request, const Wt::Http::Message& msg) SendQueue::onClientDoneSuccess(std::unique_ptr<ClientRequest> request, const Wt::Http::Message& msg)
{ {
const ClientRequestParameters& requestParameters {request->getParameters()};
bool mustThrottle{}; bool mustThrottle{};
if (msg.status() == 429) if (msg.status() == 429)
{ {
_sendQueue[request._priority].emplace_front(std::move(request)); _sendQueue[requestParameters.priority].emplace_front(std::move(request));
mustThrottle = true; mustThrottle = true;
} }
@@ -198,14 +198,14 @@ namespace Scrobbling::ListenBrainz
{ {
if (msg.status() == 200) if (msg.status() == 200)
{ {
if (request._onSuccessFunc) if (requestParameters.onSuccessFunc)
request._onSuccessFunc(msg.body()); requestParameters.onSuccessFunc(msg.body());
} }
else else
{ {
LOG(ERROR) << "Send error: '" << msg.body() << "'"; LOG(ERROR) << "Send error: '" << msg.body() << "'";
if (request._onFailureFunc) if (requestParameters.onFailureFunc)
request._onFailureFunc(); requestParameters.onFailureFunc();
} }
} }
@@ -231,7 +231,7 @@ namespace Scrobbling::ListenBrainz
} }
else if (ec) else if (ec)
{ {
throw Exception {"Throttle timer failure: " + std::string {ec.message()} }; throw LmsException {"Throttle timer failure: " + std::string {ec.message()} };
} }
_state = State::Idle; _state = State::Idle;
@@ -20,19 +20,22 @@
#pragma once #pragma once
#include <deque> #include <deque>
#include <vector>
#include <string_view>
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
#include <boost/asio/io_context_strand.hpp> #include <boost/asio/io_context_strand.hpp>
#include <boost/asio/steady_timer.hpp> #include <boost/asio/steady_timer.hpp>
#include <Wt/Http/Client.h> #include <Wt/Http/Client.h>
#include "ClientRequest.hpp"
namespace Scrobbling::ListenBrainz namespace Http
{ {
class SendQueue class SendQueue
{ {
public: public:
SendQueue(boost::asio::io_context& ioContext, std::string_view apiBaseURL); SendQueue(boost::asio::io_context& ioContext);
~SendQueue(); ~SendQueue();
SendQueue(const SendQueue&) = delete; SendQueue(const SendQueue&) = delete;
@@ -40,58 +43,15 @@ namespace Scrobbling::ListenBrainz
SendQueue& operator=(const SendQueue&) = delete; SendQueue& operator=(const SendQueue&) = delete;
SendQueue& operator=(const SendQueue&&) = delete; SendQueue& operator=(const SendQueue&&) = delete;
// generic queue operations void sendRequest(std::unique_ptr<ClientRequest> request);
struct RequestData
{
enum class Type
{
GET,
POST,
};
Type type;
std::string endpoint; // relative URL to the base API
std::vector<Wt::Http::Message::Header> headers; // used by GET
Wt::Http::Message message; // used by POST
};
class Request
{
public:
enum class Priority
{
High,
Normal,
Low,
};
Request(RequestData requestData) : _requestData {std::move(requestData)} {}
using OnSuccessFunc = std::function<void(std::string_view msgBody)>;
using OnFailureFunc = std::function<void()>;
void setOnSuccessFunc(OnSuccessFunc onSuccessFunc) { _onSuccessFunc = onSuccessFunc; }
void setOnFailureFunc(OnFailureFunc onFailureFunc) { _onFailureFunc = onFailureFunc; }
void setPriority(Priority priority) { _priority = priority; }
private:
friend class SendQueue;
RequestData _requestData;
Priority _priority {Priority::Normal};
std::size_t _retryCount {};
OnSuccessFunc _onSuccessFunc;
OnFailureFunc _onFailureFunc;
};
std::string_view getAPIBaseURL() const { return _apiBaseURL; }
void enqueueRequest(Request request);
private: private:
void sendNextQueuedRequest(); void sendNextQueuedRequest();
bool sendRequest(const RequestData& request); bool sendRequest(const ClientRequest& request);
void onClientDone(Wt::AsioWrapper::error_code ec, const Wt::Http::Message& msg); void onClientDone(Wt::AsioWrapper::error_code ec, const Wt::Http::Message& msg);
void onClientDoneError(Request request, Wt::AsioWrapper::error_code ec); void onClientDoneError(std::unique_ptr<ClientRequest> request, Wt::AsioWrapper::error_code ec);
void onClientDoneSuccess(Request request, const Wt::Http::Message& msg); void onClientDoneSuccess(std::unique_ptr<ClientRequest> request, const Wt::Http::Message& msg);
void throttle(std::chrono::seconds duration); void throttle(std::chrono::seconds duration);
const std::size_t _maxRetryCount {2}; const std::size_t _maxRetryCount {2};
@@ -99,20 +59,20 @@ namespace Scrobbling::ListenBrainz
const std::chrono::seconds _minRetryWaitDuration {1}; const std::chrono::seconds _minRetryWaitDuration {1};
const std::chrono::seconds _maxRetryWaitDuration {300}; const std::chrono::seconds _maxRetryWaitDuration {300};
boost::asio::io_context& _ioContext;
boost::asio::io_context::strand _strand {_ioContext};
boost::asio::steady_timer _throttleTimer {_ioContext};
enum class State enum class State
{ {
Idle, Idle,
Throttled, Throttled,
Sending, Sending,
}; };
boost::asio::io_context& _ioContext; State _state {State::Idle};
boost::asio::io_context::strand _strand {_ioContext}; Wt::Http::Client _client {_ioContext};
boost::asio::steady_timer _throttleTimer {_ioContext}; std::map<ClientRequestParameters::Priority, std::deque<std::unique_ptr<ClientRequest>>> _sendQueue;
std::string _apiBaseURL; std::unique_ptr<ClientRequest> _currentRequest;
State _state {State::Idle};
Wt::Http::Client _client {_ioContext};
std::map<Request::Priority, std::deque<Request>> _sendQueue;
std::optional<Request> _currentRequest;
}; };
} // namespace Scrobbling::ListenBrainz } // namespace Scrobbling::ListenBrainz
+1
View File
@@ -43,6 +43,7 @@ enum class Module
DB, DB,
DBUPDATER, DBUPDATER,
FEATURE, FEATURE,
HTTP,
MAIN, MAIN,
METADATA, METADATA,
REMOTE, REMOTE,
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include <string_view>
#include <vector>
#include <Wt/Http/Message.h>
namespace Http
{
struct ClientRequestParameters
{
enum class Priority
{
High,
Normal,
Low,
};
Priority priority {Priority::Normal};
std::string url;
using OnSuccessFunc = std::function<void(std::string_view msgBody)>;
OnSuccessFunc onSuccessFunc;
using OnFailureFunc = std::function<void()>;
OnFailureFunc onFailureFunc;
};
struct ClientGETRequestParameters final : public ClientRequestParameters
{
std::vector<Wt::Http::Message::Header> headers;
};
struct ClientPOSTRequestParameters final : public ClientRequestParameters
{
Wt::Http::Message message;
};
} // namespace Http
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string_view>
#include <boost/asio/io_context.hpp>
#include "utils/http/ClientRequestParameters.hpp"
namespace Http
{
class IClient
{
public:
virtual ~IClient() = default;
virtual void sendGETRequest(ClientGETRequestParameters&& request) = 0;
virtual void sendPOSTRequest(ClientPOSTRequestParameters&& request) = 0;
};
std::unique_ptr<IClient> createClient(boost::asio::io_context& ioContext);
} // namespace Http
+2
View File
@@ -37,6 +37,7 @@
#include "scrobbling/IScrobbling.hpp" #include "scrobbling/IScrobbling.hpp"
#include "ui/LmsApplication.hpp" #include "ui/LmsApplication.hpp"
#include "ui/LmsApplicationManager.hpp" #include "ui/LmsApplicationManager.hpp"
#include "utils/http/IClient.hpp"
#include "utils/IChildProcessManager.hpp" #include "utils/IChildProcessManager.hpp"
#include "utils/IConfig.hpp" #include "utils/IConfig.hpp"
#include "utils/IOContextRunner.hpp" #include "utils/IOContextRunner.hpp"
@@ -255,6 +256,7 @@ int main(int argc, char* argv[])
else else
throw LmsException {"Bad value '" + authenticationBackend + "' for 'authentication-backend'"}; throw LmsException {"Bad value '" + authenticationBackend + "' for 'authentication-backend'"};
Service<Http::IClient> httpClient {Http::createClient(ioContext)};
Service<CoverArt::IGrabber> coverArtService {CoverArt::createGrabber(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg")}; Service<CoverArt::IGrabber> coverArtService {CoverArt::createGrabber(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg")};
Service<Recommendation::IEngine> recommendationEngineService {Recommendation::createEngine(database)}; Service<Recommendation::IEngine> recommendationEngineService {Recommendation::createEngine(database)};
Service<Scanner::IScanner> scannerService {Scanner::createScanner(database, *recommendationEngineService)}; Service<Scanner::IScanner> scannerService {Scanner::createScanner(database, *recommendationEngineService)};