diff options
| author | Stefan Boberg <[email protected]> | 2026-04-14 16:18:23 +0200 |
|---|---|---|
| committer | Stefan Boberg <[email protected]> | 2026-04-14 16:18:23 +0200 |
| commit | 053b7373357d2555bac111b94c6909bc148f24ac (patch) | |
| tree | 456a8ce2a1b38ff6aef342324f7fa4c17fdadd30 /src/zenovermind/overmindclient.cpp | |
| parent | 5.8.4 (diff) | |
| download | zen-sb/compute-overmind.tar.xz zen-sb/compute-overmind.zip | |
Add Overmind provisioner alongside Horde and Nomadsb/compute-overmind
Introduces the zenovermind module with an HTTP client targeting the
Overmind REST gateway (/v1/jobs) and a management-thread provisioner
that schedules, polls, and cancels jobs following the same pattern as
the existing Nomad provisioner. Wired into the compute server with
full CLI options (--overmind-*), lifecycle management, and maintenance
tick support behind the ZEN_WITH_OVERMIND compile flag.
Diffstat (limited to 'src/zenovermind/overmindclient.cpp')
| -rw-r--r-- | src/zenovermind/overmindclient.cpp | 254 |
1 files changed, 254 insertions, 0 deletions
diff --git a/src/zenovermind/overmindclient.cpp b/src/zenovermind/overmindclient.cpp new file mode 100644 index 000000000..6d01437bf --- /dev/null +++ b/src/zenovermind/overmindclient.cpp @@ -0,0 +1,254 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenovermind/overmindclient.h> + +#include <zencore/fmtutils.h> +#include <zencore/iobuffer.h> +#include <zencore/logging.h> +#include <zencore/memoryview.h> +#include <zencore/trace.h> +#include <zenhttp/httpclient.h> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <json11.hpp> +ZEN_THIRD_PARTY_INCLUDES_END + +namespace zen::overmind { + +namespace { + + HttpClient::KeyValueMap MakeOvermindHeaders(const OvermindConfig& Config) + { + HttpClient::KeyValueMap Headers; + if (!Config.AuthToken.empty()) + { + Headers->emplace("Authorization", "Bearer " + Config.AuthToken); + } + return Headers; + } + +} // namespace + +OvermindClient::OvermindClient(const OvermindConfig& Config) : m_Config(Config), m_Log(zen::logging::Get("overmind.client")) +{ +} + +OvermindClient::~OvermindClient() = default; + +bool +OvermindClient::Initialize() +{ + ZEN_TRACE_CPU("OvermindClient::Initialize"); + + HttpClientSettings Settings; + Settings.LogCategory = "overmind.http"; + Settings.ConnectTimeout = std::chrono::milliseconds{10000}; + Settings.Timeout = std::chrono::milliseconds{60000}; + Settings.RetryCount = 1; + + // Ensure the base URL ends with a slash so path concatenation works correctly + std::string BaseUrl = m_Config.ServerUrl; + if (!BaseUrl.empty() && BaseUrl.back() != '/') + { + BaseUrl += '/'; + } + + m_Http = std::make_unique<zen::HttpClient>(BaseUrl, Settings); + + return true; +} + +std::string +OvermindClient::BuildJobJson(const std::string& JobName, + const std::string& OrchestratorEndpoint, + const std::string& CoordinatorSession, + bool CleanStart, + const std::string& TraceHost) const +{ + ZEN_TRACE_CPU("OvermindClient::BuildJobJson"); + + // Build the args array for the zenserver compute command + json11::Json::array Args; + Args.push_back("compute"); + Args.push_back("--http=asio"); + + if (!OrchestratorEndpoint.empty()) + { + ExtendableStringBuilder<256> CoordArg; + CoordArg << "--coordinator-endpoint=" << OrchestratorEndpoint; + Args.push_back(std::string(CoordArg.ToView())); + } + + { + ExtendableStringBuilder<128> IdArg; + IdArg << "--instance-id=overmind-" << JobName; + Args.push_back(std::string(IdArg.ToView())); + } + + if (!CoordinatorSession.empty()) + { + ExtendableStringBuilder<128> SessionArg; + SessionArg << "--coordinator-session=" << CoordinatorSession; + Args.push_back(std::string(SessionArg.ToView())); + } + + if (CleanStart) + { + Args.push_back("--clean"); + } + + if (!TraceHost.empty()) + { + ExtendableStringBuilder<128> TraceArg; + TraceArg << "--tracehost=" << TraceHost; + Args.push_back(std::string(TraceArg.ToView())); + } + + json11::Json Task = json11::Json::object{ + {"name", "zenserver"}, + {"type", "TASK_TYPE_MAIN"}, + {"command", m_Config.CommandRef}, + {"args", Args}, + {"resources", + json11::Json::object{ + {"memory", m_Config.Memory}, + {"cpu", m_Config.Cpu}, + }}, + }; + + json11::Json Body = json11::Json::object{ + {"namespace", m_Config.Namespace}, + {"region", m_Config.Region}, + {"definition", + json11::Json::object{ + {"name", JobName}, + {"os", m_Config.Os}, + {"arch", m_Config.Arch}, + {"tasks", json11::Json::array{Task}}, + }}, + }; + + return Body.dump(); +} + +bool +OvermindClient::ScheduleJob(const std::string& JobJson, OvermindJobInfo& OutJob) +{ + ZEN_TRACE_CPU("OvermindClient::ScheduleJob"); + + const IoBuffer Payload = IoBufferBuilder::MakeFromMemory(MemoryView{JobJson.data(), JobJson.size()}, ZenContentType::kJSON); + + const HttpClient::Response Response = m_Http->Post("v1/jobs", Payload, MakeOvermindHeaders(m_Config)); + + if (Response.Error) + { + ZEN_WARN("Overmind job schedule failed: {}", Response.Error->ErrorMessage); + return false; + } + + if (!Response.IsSuccess()) + { + ZEN_WARN("Overmind job schedule failed with HTTP/{}", static_cast<int>(Response.StatusCode)); + return false; + } + + const std::string Body(Response.AsText()); + std::string Err; + const json11::Json Json = json11::Json::parse(Body, Err); + + if (!Err.empty()) + { + ZEN_WARN("invalid JSON response from Overmind job schedule: {}", Err); + return false; + } + + const auto& Job = Json["job"]; + OutJob.Id = Job["id"].string_value(); + OutJob.Status = Job["status"].string_value(); + + if (OutJob.Id.empty()) + { + ZEN_WARN("Overmind job schedule response missing job ID"); + return false; + } + + ZEN_INFO("Overmind job scheduled: id={}", OutJob.Id); + + return true; +} + +bool +OvermindClient::GetJobStatus(const std::string& JobId, OvermindJobInfo& OutJob) +{ + ZEN_TRACE_CPU("OvermindClient::GetJobStatus"); + + ExtendableStringBuilder<128> Path; + Path << "v1/jobs/" << JobId; + + const HttpClient::Response Response = m_Http->Get(Path.ToView(), MakeOvermindHeaders(m_Config)); + + if (Response.Error) + { + ZEN_WARN("Overmind job status query failed for '{}': {}", JobId, Response.Error->ErrorMessage); + return false; + } + + const int StatusCode = static_cast<int>(Response.StatusCode); + + if (StatusCode == 404) + { + ZEN_INFO("Overmind job '{}' not found", JobId); + OutJob.Status = "STATUS_ERROR"; + return true; + } + + if (!Response.IsSuccess()) + { + ZEN_WARN("Overmind job status query failed with HTTP/{}", StatusCode); + return false; + } + + const std::string Body(Response.AsText()); + std::string Err; + const json11::Json Json = json11::Json::parse(Body, Err); + + if (!Err.empty()) + { + ZEN_WARN("invalid JSON in Overmind job status response: {}", Err); + return false; + } + + const auto& Job = Json["job"]; + OutJob.Id = Job["id"].string_value(); + OutJob.Status = Job["status"].string_value(); + + return true; +} + +bool +OvermindClient::CancelJob(const std::string& JobId) +{ + ZEN_TRACE_CPU("OvermindClient::CancelJob"); + + ExtendableStringBuilder<256> Path; + Path << "v1/jobs/" << JobId << "?namespace=" << m_Config.Namespace; + + const HttpClient::Response Response = m_Http->Delete(Path.ToView(), MakeOvermindHeaders(m_Config)); + + if (Response.Error) + { + ZEN_WARN("Overmind job cancel failed for '{}': {}", JobId, Response.Error->ErrorMessage); + return false; + } + + if (!Response.IsSuccess()) + { + ZEN_WARN("Overmind job cancel failed with HTTP/{}", static_cast<int>(Response.StatusCode)); + return false; + } + + ZEN_INFO("Overmind job '{}' cancelled", JobId); + return true; +} + +} // namespace zen::overmind |