Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions src/AyonCppApi/AyonCppApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@ AyonApi::AyonApi(const std::optional<std::string> &logFilePos,
m_log->info(m_log->key("AyonApi"), "Init AyonServer httplib::Client");

m_ayonServer = std::make_unique<httplib::Client>(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()) {
Expand Down Expand Up @@ -192,9 +200,15 @@ AyonApi::AyonApi(const std::optional<std::string> &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) {
Expand Down Expand Up @@ -549,6 +563,70 @@ AyonApi::batchResolvePath(std::vector<std::string> &uriPaths) {
return assetIdentGrp;
};

std::unordered_map<std::string, std::string>
AyonApi::batchResolvePathSerial(const std::vector<std::string> &uriPaths) {
PerfTimer("AyonApi::batchResolvePathSerial");
m_log->info(m_log->key("AyonApi"), "AyonApi::batchResolvePathSerial({} uris)", uriPaths.size());

std::unordered_map<std::string, std::string> assetIdentGrp;
if (uriPaths.empty()) {
return assetIdentGrp;
}

// Drop empties up front so chunk boundaries are stable and we never POST blank uris.
std::vector<std::string> 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<std::mutex> 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<std::string, std::string>
AyonApi::getAssetIdent(const nlohmann::json &uriResolverResponse) {
Expand Down
23 changes: 22 additions & 1 deletion src/AyonCppApi/AyonCppApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ class AyonApi {
*/
std::unordered_map<std::string, std::string> batchResolvePath(std::vector<std::string> &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<std::string, std::string> batchResolvePathSerial(const std::vector<std::string> &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.
Expand Down Expand Up @@ -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;
Expand Down