From 0585836550ba0770e2dddb87c097b1693a80ecc1 Mon Sep 17 00:00:00 2001 From: yw4z Date: Mon, 19 Oct 2026 23:56:52 +0300 Subject: [PATCH] Fetch cloud plugin metadata faster (#16645) When logged in to Orca Cloud, the plugin fetch at startup no longer refreshes the token a second time while start() is already refreshing it. It now waits for that refresh and checks the token again. The subscribed and owned plugin requests also run in parallel, so the fetch takes about half as long. --- src/slic3r/Utils/OrcaCloudServiceAgent.cpp | 19 ++++-- src/slic3r/plugin/CloudPluginService.cpp | 13 +++- tests/slic3rutils/test_orca_cloud_agent.cpp | 67 ++++++++++++++++++--- 3 files changed, 83 insertions(+), 16 deletions(-) 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); +}