From 54baccd6fc93ee9077eea6fb43e3dac53735d18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ba=C5=99tip=C3=A1n?= Date: Mon, 19 Aug 2024 10:02:03 +0200 Subject: [PATCH] Http: added support for retry with exponentail backoff, UserAccountCommunication::on_active triggered from app instrad of panel to correctly handle situation when multiple app windows opened, ConnectWebView: refactored the login script to prevent using old token --- src/slic3r/GUI/GUI_App.cpp | 4 ++ src/slic3r/GUI/Plater.cpp | 1 - src/slic3r/GUI/UserAccount.hpp | 2 +- src/slic3r/GUI/UserAccountCommunication.cpp | 12 ++-- src/slic3r/GUI/UserAccountCommunication.hpp | 4 +- src/slic3r/GUI/UserAccountSession.cpp | 24 ++++--- src/slic3r/GUI/UserAccountSession.hpp | 5 +- src/slic3r/GUI/WebViewDialog.cpp | 55 +++++++--------- src/slic3r/Utils/Http.cpp | 69 ++++++++++++++++++--- src/slic3r/Utils/Http.hpp | 16 ++++- 10 files changed, 127 insertions(+), 65 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 815dc42069..d107a83345 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1448,6 +1448,10 @@ bool GUI_App::on_init_inner() this->check_updates(false); }); + Bind(wxEVT_ACTIVATE_APP, [this](const wxActivateEvent &evt) { + plater_->get_user_account()->on_activate_app(evt.GetActive()); + }); + } else { #ifdef __WXMSW__ diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3ac107988e..3568b153dd 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6473,7 +6473,6 @@ void Plater::force_print_bed_update() void Plater::on_activate(bool active) { - this->p->user_account->on_activate_window(active); if (active) { this->p->show_delayed_error_message(); } diff --git a/src/slic3r/GUI/UserAccount.hpp b/src/slic3r/GUI/UserAccount.hpp index 22787f9d7c..967581f3b0 100644 --- a/src/slic3r/GUI/UserAccount.hpp +++ b/src/slic3r/GUI/UserAccount.hpp @@ -60,7 +60,7 @@ public: bool on_connect_printers_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed); bool on_connect_uiid_map_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed); - void on_activate_window(bool active) { m_communication->on_activate_window(active); } + void on_activate_app(bool active) { m_communication->on_activate_app(active); } std::string get_username() const { return m_username; } std::string get_access_token(); diff --git a/src/slic3r/GUI/UserAccountCommunication.cpp b/src/slic3r/GUI/UserAccountCommunication.cpp index 78227501aa..c5605bbf0a 100644 --- a/src/slic3r/GUI/UserAccountCommunication.cpp +++ b/src/slic3r/GUI/UserAccountCommunication.cpp @@ -475,6 +475,11 @@ void UserAccountCommunication::enqueue_refresh() BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; return; } + if (m_session->is_enqueued(UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN)) + { + BOOST_LOG_TRIVIAL(debug) << "User Account: Token refresh already enqueued, skipping..."; + return; + } m_session->enqueue_refresh({}); } wakeup_session_thread(); @@ -506,7 +511,7 @@ void UserAccountCommunication::init_session_thread() }); } -void UserAccountCommunication::on_activate_window(bool active) +void UserAccountCommunication::on_activate_app(bool active) { { std::lock_guard lck(m_thread_stop_mutex); @@ -514,12 +519,11 @@ void UserAccountCommunication::on_activate_window(bool active) } auto now = std::time(nullptr); BOOST_LOG_TRIVIAL(info) << "UserAccountCommunication activate: active " << active; - if (active && m_next_token_refresh_at - now < 60) { + if (active && m_next_token_refresh_at > 0 && m_next_token_refresh_at - now < 60) { BOOST_LOG_TRIVIAL(info) << "Enqueue access token refresh on activation"; - enqueue_refresh(); m_token_timer->Stop(); + enqueue_refresh(); } - } void UserAccountCommunication::wakeup_session_thread() diff --git a/src/slic3r/GUI/UserAccountCommunication.hpp b/src/slic3r/GUI/UserAccountCommunication.hpp index 48d9a62e39..c0859f4ca0 100644 --- a/src/slic3r/GUI/UserAccountCommunication.hpp +++ b/src/slic3r/GUI/UserAccountCommunication.hpp @@ -60,7 +60,7 @@ public: // Exchanges code for tokens and shared_session_key void on_login_code_recieved(const std::string& url_message); - void on_activate_window(bool active); + void on_activate_app(bool active); void set_username(const std::string& username); void set_remember_session(bool b); @@ -97,7 +97,7 @@ private: wxTimer* m_token_timer; wxEvtHandler* m_timer_evt_handler; - std::time_t m_next_token_refresh_at; + std::time_t m_next_token_refresh_at{0}; void wakeup_session_thread(); void init_session_thread(); diff --git a/src/slic3r/GUI/UserAccountSession.cpp b/src/slic3r/GUI/UserAccountSession.cpp index 860e90cc5d..19e92d95e4 100644 --- a/src/slic3r/GUI/UserAccountSession.cpp +++ b/src/slic3r/GUI/UserAccountSession.cpp @@ -46,7 +46,7 @@ void UserActionPost::perform(/*UNUSED*/ wxEvtHandler* evt_handler, /*UNUSED*/ co if (success_callback) success_callback(body); }); - http.perform_sync(); + http.perform_sync(HttpRetryOpt::default_retry()); } void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const @@ -69,9 +69,17 @@ void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::strin wxQueueEvent(evt_handler, new UserAccountSuccessEvent(succ_evt_type, body)); }); - http.perform_sync(); + http.perform_sync(HttpRetryOpt::default_retry()); } +bool UserAccountSession::is_enqueued(UserAccountActionID action_id) const { + return std::any_of( + std::begin(m_priority_action_queue), std::end(m_priority_action_queue), + [action_id](const ActionQueueData& item) { return item.action_id == action_id; } + ); +} + + void UserAccountSession::process_action_queue() { if (!m_proccessing_enabled) @@ -84,7 +92,7 @@ void UserAccountSession::process_action_queue() while (!m_priority_action_queue.empty()) { m_actions[m_priority_action_queue.front().action_id]->perform(p_evt_handler, m_access_token, m_priority_action_queue.front().success_callback, m_priority_action_queue.front().fail_callback, m_priority_action_queue.front().input); if (!m_priority_action_queue.empty()) - m_priority_action_queue.pop(); + m_priority_action_queue.pop_front(); } // regular queue has to wait until priority fills tokens if (!this->is_initialized()) @@ -115,7 +123,7 @@ void UserAccountSession::init_with_code(const std::string& code, const std::stri m_proccessing_enabled = true; // fail fn might be cancel_queue here - m_priority_action_queue.push({ UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN + m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN , std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1) , std::bind(&UserAccountSession::code_exchange_fail_callback, this, std::placeholders::_1) , post_fields }); @@ -188,7 +196,7 @@ void UserAccountSession::enqueue_test_with_refresh() { // on test fail - try refresh m_proccessing_enabled = true; - m_priority_action_queue.push({ UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN, nullptr, std::bind(&UserAccountSession::enqueue_refresh, this, std::placeholders::_1), {} }); + m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN, nullptr, std::bind(&UserAccountSession::enqueue_refresh, this, std::placeholders::_1), {} }); } @@ -199,7 +207,7 @@ void UserAccountSession::enqueue_refresh(const std::string& body) "&client_id=" + client_id() + "&refresh_token=" + m_refresh_token; - m_priority_action_queue.push({ UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN + m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN , std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1) , std::bind(&UserAccountSession::refresh_fail_callback, this, std::placeholders::_1) , post_fields }); @@ -218,9 +226,7 @@ void UserAccountSession::refresh_fail_callback(const std::string& body) void UserAccountSession::cancel_queue() { - while (!m_priority_action_queue.empty()) { - m_priority_action_queue.pop(); - } + m_priority_action_queue.clear(); while (!m_action_queue.empty()) { m_action_queue.pop(); } diff --git a/src/slic3r/GUI/UserAccountSession.hpp b/src/slic3r/GUI/UserAccountSession.hpp index d540319ab3..333894cded 100644 --- a/src/slic3r/GUI/UserAccountSession.hpp +++ b/src/slic3r/GUI/UserAccountSession.hpp @@ -150,7 +150,8 @@ public: void enqueue_refresh(const std::string& body); void process_action_queue(); - bool is_initialized() { return !m_access_token.empty() || !m_refresh_token.empty(); } + bool is_initialized() const { return !m_access_token.empty() || !m_refresh_token.empty(); } + bool is_enqueued(UserAccountActionID action_id) const; std::string get_access_token() const { return m_access_token; } std::string get_refresh_token() const { return m_refresh_token; } std::string get_shared_session_key() const { return m_shared_session_key; } @@ -179,7 +180,7 @@ private: long long m_next_token_timeout; std::queue m_action_queue; - std::queue m_priority_action_queue; + std::deque m_priority_action_queue; std::map> m_actions; wxEvtHandler* p_evt_handler; diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index bace551d2b..fc53db1b3b 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -636,33 +636,9 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) window.__access_token_version = 0; )", #else -// R"( -// console.log('Preparing login'); -// function errorHandler(err) { -// const msg = { -// action: 'ERROR', -// error: JSON.stringify(err), -// critical: false -// }; -// console.error('Login error occurred', msg); -// window._prusaSlicer.postMessage(msg); -// }; -// window.fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}}) -// .then(function (resp) { -// console.log('Login resp', resp); -// resp.text() -// .then(function (json) { console.log('Login resp body', json); return json; }) -// .then(function (body) { -// if (resp.status >= 400) errorHandler({status: resp.status, body}); -// }); -// }) -// .catch(function (err){ -// errorHandler({message: err.message, stack: err.stack}); -// }); -// )", + refresh ? "console.log('Refreshing login'); _prusaSlicer_initLogin('%s');" : R"( - console.log('Preparing login'); - function errorHandler(err) { + function _prusaSlicer_errorHandler(err) { const msg = { action: 'ERROR', error: JSON.stringify(err), @@ -672,13 +648,21 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) window._prusaSlicer.postMessage(msg); }; - function delay(ms) { + function _prusaSlicer_delay(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms); }); } - (async () => { + async function _prusaSlicer_initLogin(token) { + const parts = token.split('.'); + const claims = JSON.parse(atob(parts[1])); + const now = new Date().getTime() / 1000; + if (claims.exp <= now) { + console.log('Skipping initLogin as token is expired'); + return; + } + let retry = false; let backoff = 1000; const maxBackoff = 64000; @@ -688,15 +672,15 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) try { console.log('Slicer Login request'); - let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}}); + let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer ' + token}}); let body = await resp.text(); console.log('Slicer Login resp', resp.status, body); - if (resp.status >= 500) { + if (resp.status >= 500 || resp.status == 408) { retry = true; } else { retry = false; if (resp.status >= 400) - errorHandler({status: resp.status, body}); + _prusaSlicer_errorHandler({status: resp.status, body}); } } catch (e) { console.error('Slicer Login failed', e.toString()); @@ -704,13 +688,18 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh) } if (retry) { - await delay(backoff + 1000 * Math.random()); + await _prusaSlicer_delay(backoff + 1000 * Math.random()); if (backoff < maxBackoff) { backoff *= 2; } } } while (retry); - })(); + } + if (window._prusaSlicer_initialLoad === undefined) { + console.log('Initial login'); + _prusaSlicer_initLogin('%s'); + window._prusaSlicer_initialLoad = true; + } )", #endif access_token diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 2d648b9494..21de6b4ec1 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include // IWYU pragma: keep #include #include @@ -154,7 +155,7 @@ struct Http::priv std::string curl_error(CURLcode curlcode); std::string body_size_error(); - void http_perform(); + void http_perform(const HttpRetryOpt& retry_opts = HttpRetryOpt::no_retry()); }; Http::priv::priv(const std::string &url) @@ -340,8 +341,20 @@ std::string Http::priv::body_size_error() return (boost::format("HTTP body data size exceeded limit (%1% bytes)") % limit).str(); } -void Http::priv::http_perform() +bool is_transient_error(CURLcode res, long http_status) { + if (res == CURLE_OK || res == CURLE_HTTP_RETURNED_ERROR) + return http_status == 408 || http_status >= 500; + return res == CURLE_COULDNT_CONNECT || res == CURLE_COULDNT_RESOLVE_HOST || + res == CURLE_OPERATION_TIMEDOUT; +} + +void Http::priv::http_perform(const HttpRetryOpt& retry_opts) +{ + using namespace std::chrono_literals; + static thread_local std::mt19937 generator; + std::uniform_int_distribution randomized_delay(retry_opts.initial_delay.count(), (retry_opts.initial_delay.count() * 3) / 2); + ::curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); ::curl_easy_setopt(curl, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); ::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writecb); @@ -375,7 +388,28 @@ void Http::priv::http_perform() ::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, postfields.size()); } - CURLcode res = ::curl_easy_perform(curl); + bool retry; + CURLcode res; + long http_status = 0; + std::chrono::milliseconds delay = std::chrono::milliseconds(randomized_delay(generator)); + size_t num_retries = 0; + do { + res = ::curl_easy_perform(curl); + + if (res == CURLE_OK) + ::curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status); + retry = delay >= 0ms && is_transient_error(res, http_status); + if (retry && retry_opts.max_retries > 0 && num_retries >= retry_opts.max_retries) + retry = false; + if (retry) { + num_retries++; + BOOST_LOG_TRIVIAL(error) + << "HTTP Transient error (code=" << res << ", http_status=" << http_status + << "), retrying in " << delay.count() / 1000.0f << " s"; + std::this_thread::sleep_for(delay); + delay = std::min(delay * 2, retry_opts.max_delay); + } + } while (retry); putFile.reset(); @@ -397,8 +431,6 @@ void Http::priv::http_perform() if (errorfn) { errorfn(std::move(buffer), curl_error(res), 0); } }; } else { - long http_status = 0; - ::curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status); if (http_status >= 400) { if (errorfn) { errorfn(std::move(buffer), std::string(), http_status); } @@ -420,6 +452,23 @@ Http::Http(const std::string &url) : p(new priv(url)) {} // Public +const HttpRetryOpt& HttpRetryOpt::default_retry() +{ + using namespace std::chrono_literals; + static HttpRetryOpt val = {500ms, 64s, 0}; + return val; +} + +const HttpRetryOpt& HttpRetryOpt::no_retry() +{ + using namespace std::chrono_literals; + static HttpRetryOpt val = {0ms}; + return val; +} + + + + Http::Http(Http &&other) : p(std::move(other.p)) {} Http::~Http() @@ -609,13 +658,13 @@ Http& Http::cookie_jar(const std::string& file_path) return *this; } -Http::Ptr Http::perform() +Http::Ptr Http::perform(const HttpRetryOpt& retry_opts) { auto self = std::make_shared(std::move(*this)); if (self->p) { - auto io_thread = std::thread([self](){ - self->p->http_perform(); + auto io_thread = std::thread([self, &retry_opts](){ + self->p->http_perform(retry_opts); }); self->p->io_thread = std::move(io_thread); } @@ -623,9 +672,9 @@ Http::Ptr Http::perform() return self; } -void Http::perform_sync() +void Http::perform_sync(const HttpRetryOpt& retry_opts) { - if (p) { p->http_perform(); } + if (p) { p->http_perform(retry_opts); } } void Http::cancel() diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index 8e00b123fc..6d21570c12 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -11,10 +11,20 @@ #include #include #include - +#include namespace Slic3r { +struct HttpRetryOpt +{ + std::chrono::milliseconds initial_delay; + std::chrono::milliseconds max_delay; + size_t max_retries{0}; + + static const HttpRetryOpt& no_retry(); + static const HttpRetryOpt& default_retry(); +}; + /// Represetns a Http request class Http : public std::enable_shared_from_this { @@ -133,9 +143,9 @@ public: Http& set_referer(const std::string& referer); // Starts performing the request in a background thread - Ptr perform(); + Ptr perform(const HttpRetryOpt& retry_opts = HttpRetryOpt::no_retry()); // Starts performing the request on the current thread - void perform_sync(); + void perform_sync(const HttpRetryOpt &retry_opts = HttpRetryOpt::no_retry()); // Cancels a request in progress void cancel();