feat: add ClusterConfigService.WaitForQuorum

Polls GET /cluster/status for the cluster entry with quorate = 1, bounded
by a timeout (60 s default) and tolerating PveApiException while pmxcfs
and corosync restart during cluster formation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
goodolclint-claude[bot]
2026-09-01 06:20:55 +00:00
committed by GitHub
parent a4ebcec753
commit 6881e5c53c
@@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Authentication; using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client; using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Exceptions;
using PSProxmoxVE.Core.Models.Cluster; using PSProxmoxVE.Core.Models.Cluster;
using PSProxmoxVE.Core.Utilities; using PSProxmoxVE.Core.Utilities;
@@ -16,6 +18,9 @@ namespace PSProxmoxVE.Core.Services
{ {
private readonly IPveHttpClient? _injectedClient; private readonly IPveHttpClient? _injectedClient;
private static readonly TimeSpan DefaultQuorumTimeout = TimeSpan.FromSeconds(60);
private static readonly TimeSpan QuorumPollInterval = TimeSpan.FromSeconds(2);
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ClusterConfigService"/> class. /// Initializes a new instance of the <see cref="ClusterConfigService"/> class.
/// </summary> /// </summary>
@@ -378,6 +383,48 @@ namespace PSProxmoxVE.Core.Services
} }
} }
/// <summary>
/// Blocks until the cluster reports quorum (GET /cluster/status, quorate = 1).
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="timeout">Maximum time to wait. Defaults to 60 seconds.</param>
/// <remarks>
/// The cluster-create task completes before corosync converges; until the node
/// is quorate it rejects joins with "cluster not ready - no quorum?". API errors
/// during that window are transient and are retried until the deadline.
/// </remarks>
/// <exception cref="TimeoutException">Quorum was not reached before the deadline.</exception>
public void WaitForQuorum(PveSession session, TimeSpan? timeout = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
var effectiveTimeout = timeout ?? DefaultQuorumTimeout;
var deadline = DateTime.UtcNow.Add(effectiveTimeout);
while (true)
{
try
{
foreach (var entry in GetClusterStatus(session))
{
if (string.Equals(entry.Type, "cluster", StringComparison.OrdinalIgnoreCase)
&& entry.Quorate == 1)
return;
}
}
catch (PveApiException)
{
// pmxcfs and corosync restart while the cluster forms.
}
if (DateTime.UtcNow >= deadline)
throw new TimeoutException(
$"Cluster did not reach quorum within {effectiveTimeout.TotalSeconds:0} seconds.");
Thread.Sleep(QuorumPollInterval);
}
}
/// <summary> /// <summary>
/// Returns the next available VM/CT ID (GET /cluster/nextid). /// Returns the next available VM/CT ID (GET /cluster/nextid).
/// </summary> /// </summary>