aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/hub/hub.cpp
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2026-03-20 13:44:00 +0100
committerGitHub Enterprise <[email protected]>2026-03-20 13:44:00 +0100
commit7cc4b1701aa2923573adabceed486229abba5a2d (patch)
tree04a1b5eddcabd24e5c5a50a817fa50c5829972f2 /src/zenserver/hub/hub.cpp
parentZs/consul token (#870) (diff)
downloadzen-7cc4b1701aa2923573adabceed486229abba5a2d.tar.xz
zen-7cc4b1701aa2923573adabceed486229abba5a2d.zip
add hub instance info (#869)
- Improvement: Hub module listing now includes per-instance process metrics (memory, CPU time, working set, pagefile usage) - Improvement: Hub now monitors provisioned instance health in the background and refreshes process metrics periodically - Improvement: Hub no longer exposes raw `StorageServerInstance` pointers to callers; instance state is returned as value snapshots (`Hub::InstanceInfo`) - Improvement: Hub instance access is now guarded by RAII per-instance locks (`SharedLockedPtr`/`ExclusiveLockedPtr`), preventing concurrent modifications during provisioning and deprovisioning - Improvement: Hub instance lifecycle is now tracked as a `HubInstanceState` enum covering transitional states (Provisioning, Deprovisioning, Hibernating, Waking); exposed as a string in the HTTP API and dashboard
Diffstat (limited to 'src/zenserver/hub/hub.cpp')
-rw-r--r--src/zenserver/hub/hub.cpp385
1 files changed, 312 insertions, 73 deletions
diff --git a/src/zenserver/hub/hub.cpp b/src/zenserver/hub/hub.cpp
index c35fa61e8..b0208db1f 100644
--- a/src/zenserver/hub/hub.cpp
+++ b/src/zenserver/hub/hub.cpp
@@ -9,6 +9,7 @@
#include <zencore/fmtutils.h>
#include <zencore/logging.h>
#include <zencore/scopeguard.h>
+#include <zencore/timer.h>
ZEN_THIRD_PARTY_INCLUDES_START
#include <EASTL/fixed_vector.h>
@@ -150,6 +151,9 @@ Hub::Hub(const Configuration& Config,
ZEN_ASSERT(uint64_t(Config.BasePortNumber) + Config.InstanceLimit <= std::numeric_limits<uint16_t>::max());
+ m_InstanceLookup.reserve(Config.InstanceLimit);
+ m_ActiveInstances.reserve(Config.InstanceLimit);
+
m_FreePorts.resize(Config.InstanceLimit);
std::iota(m_FreePorts.begin(), m_FreePorts.end(), Config.BasePortNumber);
@@ -167,6 +171,7 @@ Hub::Hub(const Configuration& Config,
}
}
#endif
+ m_WatchDog = std::thread([this]() { WatchDog(); });
}
Hub::~Hub()
@@ -175,26 +180,43 @@ Hub::~Hub()
{
ZEN_INFO("Hub service shutting down, deprovisioning any current instances");
+ m_WatchDogEvent.Set();
+ if (m_WatchDog.joinable())
+ {
+ m_WatchDog.join();
+ }
+
+ m_WatchDog = {};
+
+ // WatchDog has been joined; no concurrent access is possible
m_Lock.WithExclusiveLock([this] {
- for (auto& [ModuleId, Instance] : m_Instances)
+ for (auto& [ModuleId, ActiveInstanceIndex] : m_InstanceLookup)
{
- uint16_t BasePort = Instance->GetBasePort();
- std::string BaseUri; // TODO?
-
- if (m_DeprovisionedModuleCallback)
+ std::unique_ptr<StorageServerInstance>& InstanceRaw = m_ActiveInstances[ActiveInstanceIndex];
{
- try
- {
- m_DeprovisionedModuleCallback(ModuleId, HubProvisionedInstanceInfo{.BaseUri = BaseUri, .Port = BasePort});
- }
- catch (const std::exception& Ex)
+ StorageServerInstance::ExclusiveLockedPtr Instance(InstanceRaw->LockExclusive(/*Wait*/ true));
+
+ uint16_t BasePort = InstanceRaw->GetBasePort();
+ std::string BaseUri; // TODO?
+
+ if (m_DeprovisionedModuleCallback)
{
- ZEN_ERROR("Deprovision callback for module {} failed. Reason: '{}'", ModuleId, Ex.what());
+ try
+ {
+ m_DeprovisionedModuleCallback(ModuleId, HubProvisionedInstanceInfo{.BaseUri = BaseUri, .Port = BasePort});
+ }
+ catch (const std::exception& Ex)
+ {
+ ZEN_ERROR("Deprovision callback for module {} failed. Reason: '{}'", ModuleId, Ex.what());
+ }
}
+ Instance.Deprovision();
}
- Instance->Deprovision();
+ InstanceRaw.reset();
}
- m_Instances.clear();
+ m_InstanceLookup.clear();
+ m_ActiveInstances.clear();
+ m_FreeActiveInstanceIndexes.clear();
});
}
catch (const std::exception& e)
@@ -206,20 +228,20 @@ Hub::~Hub()
bool
Hub::Provision(std::string_view ModuleId, HubProvisionedInstanceInfo& OutInfo, std::string& OutReason)
{
- StorageServerInstance* Instance = nullptr;
- bool IsNewInstance = false;
+ StorageServerInstance::ExclusiveLockedPtr Instance;
+ bool IsNewInstance = false;
+ uint16_t AllocatedPort = 0;
{
RwLock::ExclusiveLockScope _(m_Lock);
- uint16_t AllocatedPort = 0;
- auto RestoreAllocatedPort = MakeGuard([this, &AllocatedPort]() {
- if (AllocatedPort != 0)
+ auto RestoreAllocatedPort = MakeGuard([this, ModuleId, &IsNewInstance, &AllocatedPort]() {
+ if (IsNewInstance && AllocatedPort != 0 && !m_InstanceLookup.contains(std::string(ModuleId)))
{
m_FreePorts.push_back(AllocatedPort);
AllocatedPort = 0;
}
});
- if (auto It = m_Instances.find(std::string(ModuleId)); It == m_Instances.end())
+ if (auto It = m_InstanceLookup.find(std::string(ModuleId)); It == m_InstanceLookup.end())
{
std::string Reason;
if (!CanProvisionInstance(ModuleId, /* out */ Reason))
@@ -231,11 +253,12 @@ Hub::Provision(std::string_view ModuleId, HubProvisionedInstanceInfo& OutInfo, s
return false;
}
+ IsNewInstance = true;
+
AllocatedPort = m_FreePorts.front();
+ ZEN_ASSERT(AllocatedPort != 0);
m_FreePorts.pop_front();
- IsNewInstance = true;
-
auto NewInstance = std::make_unique<StorageServerInstance>(
m_RunEnvironment,
StorageServerInstance::Configuration{.BasePort = AllocatedPort,
@@ -245,63 +268,110 @@ Hub::Provision(std::string_view ModuleId, HubProvisionedInstanceInfo& OutInfo, s
.CoreLimit = m_Config.InstanceCoreLimit,
.ConfigPath = m_Config.InstanceConfigPath},
ModuleId);
+
#if ZEN_PLATFORM_WINDOWS
if (m_JobObject.IsValid())
{
NewInstance->SetJobObject(&m_JobObject);
}
#endif
- Instance = NewInstance.get();
- m_Instances.emplace(std::string(ModuleId), std::move(NewInstance));
- AllocatedPort = 0;
+
+ Instance = NewInstance->LockExclusive(/*Wait*/ true);
+
+ size_t ActiveInstanceIndex = (size_t)-1;
+ if (!m_FreeActiveInstanceIndexes.empty())
+ {
+ ActiveInstanceIndex = m_FreeActiveInstanceIndexes.back();
+ m_FreeActiveInstanceIndexes.pop_back();
+ ZEN_ASSERT(m_ActiveInstances.size() > ActiveInstanceIndex);
+ m_ActiveInstances[ActiveInstanceIndex] = std::move(NewInstance);
+ }
+ else
+ {
+ ActiveInstanceIndex = m_ActiveInstances.size();
+ m_ActiveInstances.emplace_back(std::move(NewInstance));
+ }
+ ZEN_ASSERT(ActiveInstanceIndex != (size_t)-1);
+ m_InstanceLookup.insert_or_assign(std::string(ModuleId), ActiveInstanceIndex);
ZEN_INFO("Created new storage server instance for module '{}'", ModuleId);
+
+ const int CurrentInstanceCount = gsl::narrow_cast<int>(m_InstanceLookup.size());
+ int CurrentMaxCount = m_MaxInstanceCount.load();
+ const int NewMax = Max(CurrentMaxCount, CurrentInstanceCount);
+
+ m_MaxInstanceCount.compare_exchange_weak(CurrentMaxCount, NewMax);
}
else
{
- Instance = It->second.get();
+ const size_t ActiveInstanceIndex = It->second;
+ ZEN_ASSERT(m_ActiveInstances.size() > ActiveInstanceIndex);
+
+ std::unique_ptr<StorageServerInstance>& InstanceRaw = m_ActiveInstances[ActiveInstanceIndex];
+ Instance = InstanceRaw->LockExclusive(/*Wait*/ true);
+ AllocatedPort = InstanceRaw->GetBasePort();
}
m_ProvisioningModules.emplace(std::string(ModuleId));
}
- ZEN_ASSERT(Instance != nullptr);
+ ZEN_ASSERT(Instance);
auto RemoveProvisioningModule = MakeGuard([&] {
RwLock::ExclusiveLockScope _(m_Lock);
m_ProvisioningModules.erase(std::string(ModuleId));
+ if (IsNewInstance && AllocatedPort != 0 && !m_InstanceLookup.contains(std::string(ModuleId)))
+ {
+ m_FreePorts.push_back(AllocatedPort);
+ AllocatedPort = 0;
+ }
});
- // NOTE: this is done while not holding the lock, as provisioning may take time
+ // NOTE: this is done while not holding the hub lock, as provisioning may take time
// and we don't want to block other operations. We track which modules are being
// provisioned using m_ProvisioningModules, and reject attempts to provision/deprovision
// those modules while in this state.
- UpdateStats();
-
try
{
- Instance->Provision();
+ Instance.Provision();
+ Instance = {};
}
catch (const std::exception& Ex)
{
ZEN_ERROR("Failed to provision storage server instance for module '{}': {}", ModuleId, Ex.what());
+ Instance = {};
+
if (IsNewInstance)
{
- // Clean up
- RwLock::ExclusiveLockScope _(m_Lock);
- if (auto It = m_Instances.find(std::string(ModuleId)); It != m_Instances.end())
+ // Clean up failed instance provisioning
+ std::unique_ptr<StorageServerInstance> DestroyInstance;
+ {
+ RwLock::ExclusiveLockScope _(m_Lock);
+ if (auto It = m_InstanceLookup.find(std::string(ModuleId)); It != m_InstanceLookup.end())
+ {
+ const size_t ActiveInstanceIndex = It->second;
+ ZEN_ASSERT(ActiveInstanceIndex < m_ActiveInstances.size());
+ DestroyInstance = std::move(m_ActiveInstances[ActiveInstanceIndex]);
+ ZEN_ASSERT(DestroyInstance);
+ ZEN_ASSERT(!m_ActiveInstances[ActiveInstanceIndex]);
+ m_FreeActiveInstanceIndexes.push_back(ActiveInstanceIndex);
+ m_InstanceLookup.erase(It);
+ }
+ }
+ try
+ {
+ DestroyInstance.reset();
+ }
+ catch (const std::exception& Ex)
{
- ZEN_ASSERT(It->second != nullptr);
- uint16_t BasePort = It->second->GetBasePort();
- m_FreePorts.push_back(BasePort);
- m_Instances.erase(It);
+ ZEN_ERROR("Failed to destroy instance for failed provision module '{}': {}", ModuleId, Ex.what());
}
}
return false;
}
- OutInfo.Port = Instance->GetBasePort();
+ OutInfo.Port = AllocatedPort;
// TODO: base URI? Would need to know what host name / IP to use
if (m_ProvisionedModuleCallback)
@@ -322,7 +392,8 @@ Hub::Provision(std::string_view ModuleId, HubProvisionedInstanceInfo& OutInfo, s
bool
Hub::Deprovision(const std::string& ModuleId, std::string& OutReason)
{
- std::unique_ptr<StorageServerInstance> Instance;
+ std::unique_ptr<StorageServerInstance> RawInstance;
+ StorageServerInstance::ExclusiveLockedPtr Instance;
{
RwLock::ExclusiveLockScope _(m_Lock);
@@ -336,22 +407,31 @@ Hub::Deprovision(const std::string& ModuleId, std::string& OutReason)
return false;
}
- if (auto It = m_Instances.find(ModuleId); It == m_Instances.end())
+ if (auto It = m_InstanceLookup.find(ModuleId); It == m_InstanceLookup.end())
{
ZEN_WARN("Attempted to deprovision non-existent module '{}'", ModuleId);
- // Not found, OutReason should be empty
+ // Not found, OutReason left empty
return false;
}
else
{
- Instance = std::move(It->second);
- m_Instances.erase(It);
+ const size_t ActiveInstanceIndex = It->second;
+ ZEN_ASSERT(ActiveInstanceIndex < m_ActiveInstances.size());
+ RawInstance = std::move(m_ActiveInstances[ActiveInstanceIndex]);
+ ZEN_ASSERT(RawInstance != nullptr);
+ m_FreeActiveInstanceIndexes.push_back(ActiveInstanceIndex);
+ m_InstanceLookup.erase(It);
m_DeprovisioningModules.emplace(ModuleId);
+
+ Instance = RawInstance->LockExclusive(/*Wait*/ true);
}
}
- uint16_t BasePort = Instance->GetBasePort();
+ ZEN_ASSERT(RawInstance);
+ ZEN_ASSERT(Instance);
+
+ uint16_t BasePort = RawInstance->GetBasePort();
std::string BaseUri; // TODO?
if (m_DeprovisionedModuleCallback)
@@ -366,57 +446,82 @@ Hub::Deprovision(const std::string& ModuleId, std::string& OutReason)
}
}
- // The module is deprovisioned outside the lock to avoid blocking other operations.
+ // The module is deprovisioned outside the hub lock to avoid blocking other operations.
//
// To ensure that no new provisioning can occur while we're deprovisioning,
// we add the module ID to m_DeprovisioningModules and remove it once
// deprovisioning is complete.
auto _ = MakeGuard([&] {
- RwLock::ExclusiveLockScope _(m_Lock);
- m_DeprovisioningModules.erase(ModuleId);
- m_FreePorts.push_back(BasePort);
+ {
+ RwLock::ExclusiveLockScope _(m_Lock);
+ m_DeprovisioningModules.erase(ModuleId);
+ m_FreePorts.push_back(BasePort);
+ }
});
- Instance->Deprovision();
+ Instance.Deprovision();
+
+ Instance = {};
return true;
}
bool
-Hub::Find(std::string_view ModuleId, StorageServerInstance** OutInstance)
+Hub::Find(std::string_view ModuleId, InstanceInfo* OutInstanceInfo)
{
RwLock::SharedLockScope _(m_Lock);
- if (auto It = m_Instances.find(std::string(ModuleId)); It != m_Instances.end())
+ if (auto It = m_InstanceLookup.find(std::string(ModuleId)); It != m_InstanceLookup.end())
{
- if (OutInstance)
+ if (OutInstanceInfo)
{
- *OutInstance = It->second.get();
+ const size_t ActiveInstanceIndex = It->second;
+ ZEN_ASSERT(ActiveInstanceIndex < m_ActiveInstances.size());
+ const std::unique_ptr<StorageServerInstance>& Instance = m_ActiveInstances[ActiveInstanceIndex];
+ ZEN_ASSERT(Instance);
+ InstanceInfo Info{
+ Instance->GetState(),
+ std::chrono::system_clock::now() // TODO
+ };
+ Instance->GetProcessMetrics(Info.Metrics);
+
+ *OutInstanceInfo = Info;
}
return true;
}
- else if (OutInstance)
- {
- *OutInstance = nullptr;
- }
return false;
}
void
-Hub::EnumerateModules(std::function<void(StorageServerInstance&)> Callback)
+Hub::EnumerateModules(std::function<void(std::string_view ModuleId, const InstanceInfo&)> Callback)
{
- RwLock::SharedLockScope _(m_Lock);
- for (auto& It : m_Instances)
+ std::vector<std::pair<std::string, InstanceInfo>> Infos;
+ {
+ RwLock::SharedLockScope _(m_Lock);
+ for (auto& [ModuleId, ActiveInstanceIndex] : m_InstanceLookup)
+ {
+ const std::unique_ptr<StorageServerInstance>& Instance = m_ActiveInstances[ActiveInstanceIndex];
+ ZEN_ASSERT(Instance);
+ InstanceInfo Info{
+ Instance->GetState(),
+ std::chrono::system_clock::now() // TODO
+ };
+ Instance->GetProcessMetrics(Info.Metrics);
+
+ Infos.push_back(std::make_pair(std::string(Instance->GetModuleId()), Info));
+ }
+ }
+
+ for (const std::pair<std::string, InstanceInfo>& Info : Infos)
{
- Callback(*It.second);
+ Callback(Info.first, Info.second);
}
}
int
Hub::GetInstanceCount()
{
- RwLock::SharedLockScope _(m_Lock);
- return gsl::narrow_cast<int>(m_Instances.size());
+ return m_Lock.WithSharedLock([this]() { return gsl::narrow_cast<int>(m_InstanceLookup.size()); });
}
void
@@ -424,13 +529,19 @@ Hub::UpdateCapacityMetrics()
{
m_HostMetrics = GetSystemMetrics();
- // Update per-instance metrics
+ // TODO: Should probably go into WatchDog and use atomic for update so it can be read without locks...
+ // Per-instance stats are already refreshed by WatchDog and are readable via the Find and EnumerateModules
}
void
Hub::UpdateStats()
{
- m_Lock.WithSharedLock([this] { m_MaxInstanceCount = Max(m_MaxInstanceCount, gsl::narrow_cast<int>(m_Instances.size())); });
+ int CurrentInstanceCount = m_Lock.WithSharedLock([this] { return gsl::narrow_cast<int>(m_InstanceLookup.size()); });
+ int CurrentMaxCount = m_MaxInstanceCount.load();
+
+ int NewMax = Max(CurrentMaxCount, CurrentInstanceCount);
+
+ m_MaxInstanceCount.compare_exchange_weak(CurrentMaxCount, NewMax);
}
bool
@@ -450,19 +561,19 @@ Hub::CanProvisionInstance(std::string_view ModuleId, std::string& OutReason)
return false;
}
- if (gsl::narrow_cast<int>(m_Instances.size()) >= m_Config.InstanceLimit)
+ if (gsl::narrow_cast<int>(m_InstanceLookup.size()) >= m_Config.InstanceLimit)
{
OutReason = fmt::format("instance limit ({}) exceeded", m_Config.InstanceLimit);
return false;
}
- // Since deprovisioning happens outside the lock and we don't add the port back until the instance is full shut down we might be under
- // the instance limit but all ports may be in use
+ // Since deprovisioning happens outside the lock and we don't return the port until the instance is fully shut down, we might be below
+ // the instance count limit but with no free ports available
if (m_FreePorts.empty())
{
OutReason = fmt::format("no free ports available, deprovisioning of instances might be in flight ({})",
- m_Config.InstanceLimit - m_Instances.size());
+ m_Config.InstanceLimit - m_InstanceLookup.size());
return false;
}
@@ -472,6 +583,71 @@ Hub::CanProvisionInstance(std::string_view ModuleId, std::string& OutReason)
return true;
}
+void
+Hub::WatchDog()
+{
+ constexpr uint64_t WatchDogWakeupTimeMs = 5000;
+ constexpr uint64_t WatchDogProcessingTimeMs = 500;
+
+ size_t CheckInstanceIndex = 0;
+ while (!m_WatchDogEvent.Wait(WatchDogWakeupTimeMs))
+ {
+ try
+ {
+ size_t MaxCheckCount = m_Lock.WithSharedLock([this]() { return m_InstanceLookup.size(); });
+
+ Stopwatch Timer;
+ while (MaxCheckCount-- > 0 && Timer.GetElapsedTimeMs() < WatchDogProcessingTimeMs && !m_WatchDogEvent.Wait(5))
+ {
+ StorageServerInstance::SharedLockedPtr LockedInstance;
+ m_Lock.WithSharedLock([this, &CheckInstanceIndex, &LockedInstance]() {
+ if (m_InstanceLookup.empty())
+ {
+ return;
+ }
+
+ size_t MaxLoopCount = m_ActiveInstances.size();
+ StorageServerInstance* Instance = nullptr;
+ while (MaxLoopCount-- > 0 && !Instance)
+ {
+ CheckInstanceIndex++;
+ if (CheckInstanceIndex >= m_ActiveInstances.size())
+ {
+ CheckInstanceIndex = 0;
+ }
+ Instance = (CheckInstanceIndex < m_ActiveInstances.size()) ? m_ActiveInstances[CheckInstanceIndex].get() : nullptr;
+ }
+
+ if (Instance)
+ {
+ LockedInstance = Instance->LockShared(/*Wait*/ false);
+ }
+ });
+
+ if (LockedInstance)
+ {
+ if (LockedInstance.IsRunning())
+ {
+ LockedInstance.UpdateMetrics();
+ }
+ else if (LockedInstance.GetState() == HubInstanceState::Provisioned)
+ {
+ // Process is not running but state says it should be — instance died unexpectedly.
+ // TODO: Track and attempt recovery.
+ }
+ // else: transitional state (Provisioning, Deprovisioning, Hibernating, Waking) — expected, skip.
+ LockedInstance = {};
+ }
+ }
+ }
+ catch (const std::exception& Ex)
+ {
+ // TODO: Catch specific errors such as asserts, OOM, OOD, system_error etc
+ ZEN_ERROR("Hub watchdog threw exception: {}", Ex.what());
+ }
+ }
+}
+
#if ZEN_WITH_TESTS
TEST_SUITE_BEGIN("server.hub");
@@ -507,7 +683,9 @@ TEST_CASE("hub.provision_basic")
REQUIRE_MESSAGE(ProvisionResult, Reason);
CHECK_NE(Info.Port, 0);
CHECK_EQ(HubInstance->GetInstanceCount(), 1);
- CHECK(HubInstance->Find("module_a"));
+ Hub::InstanceInfo InstanceInfo;
+ REQUIRE(HubInstance->Find("module_a", &InstanceInfo));
+ CHECK_EQ(InstanceInfo.State, HubInstanceState::Provisioned);
const bool DeprovisionResult = HubInstance->Deprovision("module_a", Reason);
CHECK(DeprovisionResult);
@@ -541,7 +719,9 @@ TEST_CASE("hub.provision_config")
REQUIRE_MESSAGE(ProvisionResult, Reason);
CHECK_NE(Info.Port, 0);
CHECK_EQ(HubInstance->GetInstanceCount(), 1);
- CHECK(HubInstance->Find("module_a"));
+ Hub::InstanceInfo InstanceInfo;
+ REQUIRE(HubInstance->Find("module_a", &InstanceInfo));
+ CHECK_EQ(InstanceInfo.State, HubInstanceState::Provisioned);
HttpClient Client(fmt::format("http://127.0.0.1:{}{}", Info.Port, Info.BaseUri));
HttpClient::Response TestResponse = Client.Get("/status/builds");
@@ -660,7 +840,10 @@ TEST_CASE("hub.enumerate_modules")
REQUIRE_MESSAGE(HubInstance->Provision("enum_b", Info, Reason), Reason);
std::vector<std::string> Ids;
- HubInstance->EnumerateModules([&](StorageServerInstance& Instance) { Ids.push_back(std::string(Instance.GetModuleId())); });
+ HubInstance->EnumerateModules([&](std::string_view ModuleId, const Hub::InstanceInfo& Info) {
+ Ids.push_back(std::string(ModuleId));
+ CHECK_EQ(Info.State, HubInstanceState::Provisioned);
+ });
CHECK_EQ(Ids.size(), 2u);
const bool FoundA = std::find(Ids.begin(), Ids.end(), "enum_a") != Ids.end();
const bool FoundB = std::find(Ids.begin(), Ids.end(), "enum_b") != Ids.end();
@@ -669,7 +852,10 @@ TEST_CASE("hub.enumerate_modules")
HubInstance->Deprovision("enum_a", Reason);
Ids.clear();
- HubInstance->EnumerateModules([&](StorageServerInstance& Instance) { Ids.push_back(std::string(Instance.GetModuleId())); });
+ HubInstance->EnumerateModules([&](std::string_view ModuleId, const Hub::InstanceInfo& Info) {
+ Ids.push_back(std::string(ModuleId));
+ CHECK_EQ(Info.State, HubInstanceState::Provisioned);
+ });
REQUIRE_EQ(Ids.size(), 1u);
CHECK_EQ(Ids[0], "enum_b");
}
@@ -936,6 +1122,59 @@ TEST_CASE("hub.job_object")
}
# endif // ZEN_PLATFORM_WINDOWS
+TEST_CASE("hub.instance_state_basic")
+{
+ ScopedTemporaryDirectory TempDir;
+ Hub::Configuration Config;
+ Config.BasePortNumber = 22400;
+ std::unique_ptr<Hub> HubInstance = hub_testutils::MakeHub(TempDir.Path(), Config);
+
+ HubProvisionedInstanceInfo ProvInfo;
+ Hub::InstanceInfo Info;
+ std::string Reason;
+
+ REQUIRE_MESSAGE(HubInstance->Provision("state_a", ProvInfo, Reason), Reason);
+
+ REQUIRE(HubInstance->Find("state_a", &Info));
+ CHECK_EQ(Info.State, HubInstanceState::Provisioned);
+
+ HubInstance->Deprovision("state_a", Reason);
+ CHECK_FALSE(HubInstance->Find("state_a"));
+}
+
+TEST_CASE("hub.instance_state_enumerate")
+{
+ ScopedTemporaryDirectory TempDir;
+ Hub::Configuration Config;
+ Config.BasePortNumber = 22500;
+ std::unique_ptr<Hub> HubInstance = hub_testutils::MakeHub(TempDir.Path(), Config);
+
+ HubProvisionedInstanceInfo ProvInfo;
+ std::string Reason;
+ REQUIRE_MESSAGE(HubInstance->Provision("estate_a", ProvInfo, Reason), Reason);
+ REQUIRE_MESSAGE(HubInstance->Provision("estate_b", ProvInfo, Reason), Reason);
+
+ int ProvisionedCount = 0;
+ HubInstance->EnumerateModules([&](std::string_view, const Hub::InstanceInfo& InstanceInfo) {
+ if (InstanceInfo.State == HubInstanceState::Provisioned)
+ {
+ ProvisionedCount++;
+ }
+ });
+ CHECK_EQ(ProvisionedCount, 2);
+
+ HubInstance->Deprovision("estate_a", Reason);
+
+ ProvisionedCount = 0;
+ HubInstance->EnumerateModules([&](std::string_view, const Hub::InstanceInfo& InstanceInfo) {
+ if (InstanceInfo.State == HubInstanceState::Provisioned)
+ {
+ ProvisionedCount++;
+ }
+ });
+ CHECK_EQ(ProvisionedCount, 1);
+}
+
TEST_SUITE_END();
void