diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index f5f7e30d1d..f759d15a2b 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -1917,13 +1917,20 @@ RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& rea bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const std::string& reason) { - bool needs_refresh = false; - { + const auto needs_refresh = [&] { std::lock_guard lock(session_mutex); - needs_refresh = should_refresh_locked(skew); - } + return should_refresh_locked(skew); + }; + if (!needs_refresh()) + return true; - if (!needs_refresh) + // A refresh may already be running on another thread, e.g. the one start() launches. Wait for + // it and check again instead of spending the refresh token it is rotating. Bounded: the + // refresh POST times out after 30 s. + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60); + while (refresh_running.load() && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + if (!needs_refresh()) return true; // First attempt. refresh_from_storage blocks on the cross-process lock and reads the @@ -1935,6 +1942,8 @@ bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const // One retry, only for a transient network failure of the refresh POST (lost response, // timeout, 429/5xx -> Transient). The retry re-acquires the lock and re-reads the store. std::this_thread::sleep_for(std::chrono::milliseconds(750)); + if (!needs_refresh()) + return true; return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success; } diff --git a/src/slic3r/plugin/CloudPluginService.cpp b/src/slic3r/plugin/CloudPluginService.cpp index 22e0b57f81..221ec7787c 100644 --- a/src/slic3r/plugin/CloudPluginService.cpp +++ b/src/slic3r/plugin/CloudPluginService.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -49,16 +50,22 @@ bool CloudPluginService::fetch_manifests_into_descriptors(std::vectorensure_token_fresh("fetch_plugin_manifests"); + std::vector mine_descriptors; + auto mine = std::async(std::launch::async, [agent = m_orca_agent, &mine_descriptors] { + return agent->fetch_mine_manifests_into_descriptors(mine_descriptors); + }); + int ret = m_orca_agent->fetch_subscribed_manifests_into_descriptors(descriptors, not_found, unauthorized); + const int mine_ret = mine.get(); if (ret != 0) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get subscribed plugins."; return false; } - std::vector mine_descriptors; - - ret = m_orca_agent->fetch_mine_manifests_into_descriptors(mine_descriptors); + ret = mine_ret; if (ret != 0) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get owned plugins."; diff --git a/tests/slic3rutils/test_orca_cloud_agent.cpp b/tests/slic3rutils/test_orca_cloud_agent.cpp index 485b662c0b..f84a14ae76 100644 --- a/tests/slic3rutils/test_orca_cloud_agent.cpp +++ b/tests/slic3rutils/test_orca_cloud_agent.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -91,7 +92,9 @@ using boost::asio::ip::tcp; class LoopbackServer { public: - enum class Mode { Ok, Unavailable, Silent }; + // Session answers every request with the body given to set_session_body(), as the token endpoint + // does, after 200 ms so the client is still waiting when the test makes its next call. + enum class Mode { Ok, Unavailable, Silent, Session }; struct Request { @@ -113,6 +116,11 @@ public: std::string url() const { return "http://127.0.0.1:" + std::to_string(m_acceptor.local_endpoint().port()); } void set_mode(Mode mode) { m_mode = mode; } + void set_session_body(std::string body) + { + std::lock_guard lock(m_mutex); + m_session_body = std::move(body); + } bool wait_for_accepts(size_t count, std::chrono::milliseconds timeout) { @@ -172,13 +180,31 @@ private: const Mode mode = m_mode; if (mode == Mode::Silent) return; - const std::string_view response = mode == Mode::Ok ? - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}" : - "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; - boost::asio::async_write(connection->socket, boost::asio::buffer(response), - [connection](boost::system::error_code, size_t) { - boost::system::error_code ignored; - connection->socket.shutdown(tcp::socket::shutdown_both, ignored); + auto response = std::make_shared(); + if (mode == Mode::Session) { + std::lock_guard lock(m_mutex); + *response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " + + std::to_string(m_session_body.size()) + "\r\nConnection: close\r\n\r\n" + m_session_body; + } else { + *response = mode == Mode::Ok ? + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}" : + "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; + } + auto write = [connection, response] { + boost::asio::async_write(connection->socket, boost::asio::buffer(*response), + [connection, response](boost::system::error_code, size_t) { + boost::system::error_code ignored; + connection->socket.shutdown(tcp::socket::shutdown_both, ignored); + }); + }; + if (mode != Mode::Session) { + write(); + return; + } + auto timer = std::make_shared(m_io, 200ms); + timer->async_wait([timer, write](boost::system::error_code ec) { + if (!ec) + write(); }); }); } @@ -192,8 +218,12 @@ private: std::condition_variable m_cv; std::vector m_requests; std::vector> m_connections; + std::string m_session_body; }; +// A JWT whose only claim is {"exp":4102444800} (2100-01-01); the agent reads nothing else from it. +constexpr const char* FAR_FUTURE_JWT = "eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.sig"; + // What the agent reported through its callbacks, recorded on whichever thread delivers it so the // assertions can run on the test thread. struct CallbackLog @@ -515,3 +545,24 @@ TEST_CASE_METHOD(HealthCheckFixture, "The health check does no token work", "[Or CHECK(agent->is_user_login()); CHECK(agent->get_access_token() == "test-token"); } + +TEST_CASE_METHOD(HealthCheckFixture, "A token request made while start() refreshes waits for it instead of refreshing again", "[OrcaCloudServiceAgent]") +{ + // A session saved by a previous run: start() refreshes it on its own thread. + agent->persist_user_secret(nlohmann::json{{"refresh_token", "stored-refresh-token"}, {"user_id", "test-user-id"}}.dump()); + const std::string access_token = FAR_FUTURE_JWT; + server.set_session_body(nlohmann::json{{"access_token", access_token}, + {"refresh_token", "rotated-refresh-token"}, + {"user_id", "test-user-id"}}.dump()); + server.set_mode(LoopbackServer::Mode::Session); + + REQUIRE(agent->start() == BAMBU_NETWORK_SUCCESS); + CHECK(agent->ensure_token_fresh("test")); + CHECK(agent->get_access_token() == access_token); + + size_t token_posts = 0; + for (const auto& request : server.requests()) + if (boost::starts_with(request.request_line, "POST /auth/v1/token")) + ++token_posts; + CHECK(token_posts == 1); +}