diff --git a/src/AyonCppApi/AyonCppApi.cpp b/src/AyonCppApi/AyonCppApi.cpp index c39d33f..0563d37 100644 --- a/src/AyonCppApi/AyonCppApi.cpp +++ b/src/AyonCppApi/AyonCppApi.cpp @@ -153,6 +153,14 @@ AyonApi::AyonApi(const std::optional &logFilePos, m_log->info(m_log->key("AyonApi"), "Init AyonServer httplib::Client"); m_ayonServer = std::make_unique(m_serverUrl); + // Reuse the TCP/TLS connection across resolves instead of a fresh handshake per + // request. Over a WAN link the handshake dominates per-call latency, so this is + // a large win for the serial resolve path and the prewarm batched resolves. + // Gated so the keep-alive contribution can be benchmarked: set + // AYON_RESOLVER_NO_KEEPALIVE=1 to fall back to a fresh handshake per request. + if (!std::getenv("AYON_RESOLVER_NO_KEEPALIVE")) { + m_ayonServer->set_keep_alive(true); + } m_log->info(m_log->key("AyonApi"), "After creating httplib::Client - {}", m_serverUrl); if (isSSL()) { @@ -192,9 +200,15 @@ AyonApi::AyonApi(const std::optional &logFilePos, m_log->info(m_log->key("AyonApi"), "Status code: {}", res->status); m_headers = { - {"X-Api-Key", m_authKey}, - {"X-ayon-site-id", m_siteId} + {"X-Api-Key", m_authKey} }; + // Only advertise a site id when we actually have one. A service-account + // setup (or any machine without a registered AYON site) has no valid + // site, and the server returns 400 "Invalid site id" if the header is + // present but empty/unknown. Omitting it lets resolution proceed. + if (!m_siteId.empty()) { + m_headers.emplace("X-ayon-site-id", m_siteId); + } auto resMe = m_ayonServer->Get("/api/users/me", m_headers); if (resMe && resMe->status != 200) { @@ -549,6 +563,70 @@ AyonApi::batchResolvePath(std::vector &uriPaths) { return assetIdentGrp; }; +std::unordered_map +AyonApi::batchResolvePathSerial(const std::vector &uriPaths) { + PerfTimer("AyonApi::batchResolvePathSerial"); + m_log->info(m_log->key("AyonApi"), "AyonApi::batchResolvePathSerial({} uris)", uriPaths.size()); + + std::unordered_map assetIdentGrp; + if (uriPaths.empty()) { + return assetIdentGrp; + } + + // Drop empties up front so chunk boundaries are stable and we never POST blank uris. + std::vector cleanUris; + cleanUris.reserve(uriPaths.size()); + for (const auto &uri: uriPaths) { + if (!uri.empty()) { + cleanUris.push_back(uri); + } + } + if (cleanUris.empty()) { + return assetIdentGrp; + } + + const std::string endPoint + = m_pathOnlyResolution ? m_uriResolverEndpoint + m_uriResolverEndpointPathOnlyVar : m_uriResolverEndpoint; + + // Split large frontiers into capped chunks (see m_maxSerialBatchSize). Each chunk is its + // own POST over the keep-alive client, so the server processes and releases a bounded + // batch at a time. For typical frontiers (<= m_maxSerialBatchSize) this is a single + // request, identical to the un-chunked path. + const size_t chunkSize = m_maxSerialBatchSize; + for (size_t start = 0; start < cleanUris.size(); start += chunkSize) { + const size_t end = std::min(start + chunkSize, cleanUris.size()); + + nlohmann::json uriArray = nlohmann::json::array(); + for (size_t i = start; i < end; ++i) { + uriArray.push_back(cleanUris[i]); + } + nlohmann::json jsonPayload = {{"resolveRoots", false}, {"uris", uriArray}}; + std::string payload = jsonPayload.dump(); + + std::string rawResponse; + { + std::lock_guard lock(m_ayonServerMutex); + rawResponse = serialCorePost(endPoint, m_headers, payload, 200); + } + if (rawResponse.empty()) { + m_log->warn("AyonApi::batchResolvePathSerial empty response for chunk [{}, {})", start, end); + continue; + } + + try { + nlohmann::json responseArray = nlohmann::json::parse(rawResponse); + for (const auto &assetRaw: responseArray) { + assetIdentGrp.emplace(getAssetIdent(assetRaw)); + } + } + catch (const nlohmann::json::exception &e) { + m_log->error("AyonApi::batchResolvePathSerial JSON parse failed: {}", e.what()); + } + } + + return assetIdentGrp; +}; + // TODO make it so that hero version is chosen if available std::pair AyonApi::getAssetIdent(const nlohmann::json &uriResolverResponse) { diff --git a/src/AyonCppApi/AyonCppApi.h b/src/AyonCppApi/AyonCppApi.h index 2f7b347..4714496 100644 --- a/src/AyonCppApi/AyonCppApi.h +++ b/src/AyonCppApi/AyonCppApi.h @@ -110,6 +110,21 @@ class AyonApi { */ std::unordered_map batchResolvePath(std::vector &uriPaths); + /** + * @brief Resolves a vector of paths in a SINGLE batched request over the persistent + * keep-alive client. + * + * Unlike batchResolvePath (which fans out parallel requests on fresh, non-keep-alive + * clients), this sends one POST with all URIs through m_ayonServer and parses the whole + * response array. One round-trip, deterministic, connection reused. Intended for the + * prewarm pass where frontiers are modest and determinism + keep-alive matter more than + * request-level parallelism. + * + * @param uriPaths The vector of URI paths to resolve. + * @return An unordered map of URI -> resolved path. + */ + std::unordered_map batchResolvePathSerial(const std::vector &uriPaths); + /** * @brief Takes an AYON path URI response (resolved ayon://path) and returns a pair of * asset identifier (ayon:// path) and the machine local file location. @@ -214,7 +229,13 @@ class AyonApi { uint16_t m_regroupSizeForAsyncRequests = 200; uint16_t m_maxGroupSizeForAsyncRequests = 300; uint16_t m_minVecSizeForGroupSplitAsyncRequests = 50; - + // Max uris per POST in batchResolvePathSerial. The server resolves a batch in one + // sequential transaction, so an unbounded request holds a DB connection for the whole + // frontier (risking its connection-pool 503 guard) and grows the body/response without + // limit. Splitting into capped chunks releases the connection between chunks. Matches + // m_regroupSizeForAsyncRequests so the serial and async paths cap requests alike. + uint16_t m_maxSerialBatchSize = 200; + // Retry and Timeout Configuration uint8_t m_maxCallRetries = 8; uint16_t m_retryWait = 800;