81 lines
3.1 KiB
C#
81 lines
3.1 KiB
C#
namespace NATS.Server.JetStream.Cluster;
|
|
|
|
/// <summary>
|
|
/// Topology-aware peer selection for stream/consumer replica placement.
|
|
/// Go reference: jetstream_cluster.go:7212 selectPeerGroup.
|
|
/// </summary>
|
|
public static class PlacementEngine
|
|
{
|
|
/// <summary>
|
|
/// Selects peers for a new replica group based on available nodes, tags, and cluster affinity.
|
|
/// Filters unavailable peers, applies cluster/tag/exclude-tag policy, then picks the top N
|
|
/// peers ordered by available storage descending.
|
|
/// </summary>
|
|
public static RaftGroup SelectPeerGroup(
|
|
string groupName,
|
|
int replicas,
|
|
IReadOnlyList<PeerInfo> availablePeers,
|
|
PlacementPolicy? policy = null)
|
|
{
|
|
// 1. Filter out unavailable peers.
|
|
IEnumerable<PeerInfo> candidates = availablePeers.Where(p => p.Available);
|
|
|
|
// 2. If policy has Cluster, filter to matching cluster.
|
|
if (policy?.Cluster is { Length: > 0 } cluster)
|
|
candidates = candidates.Where(p => string.Equals(p.Cluster, cluster, StringComparison.OrdinalIgnoreCase));
|
|
|
|
// 3. If policy has Tags, filter to peers that have ALL required tags.
|
|
if (policy?.Tags is { Count: > 0 } requiredTags)
|
|
candidates = candidates.Where(p => requiredTags.All(tag => p.Tags.Contains(tag)));
|
|
|
|
// 4. If policy has ExcludeTags, filter out peers with any of those tags.
|
|
if (policy?.ExcludeTags is { Count: > 0 } excludeTags)
|
|
candidates = candidates.Where(p => !excludeTags.Any(tag => p.Tags.Contains(tag)));
|
|
|
|
// 5. If not enough peers after filtering, throw InvalidOperationException.
|
|
var filtered = candidates.ToList();
|
|
if (filtered.Count < replicas)
|
|
throw new InvalidOperationException(
|
|
$"Not enough peers available to satisfy replica count {replicas}. " +
|
|
$"Available after policy filtering: {filtered.Count}.");
|
|
|
|
// 6. Sort remaining by available storage descending.
|
|
var selected = filtered
|
|
.OrderByDescending(p => p.AvailableStorage)
|
|
.Take(replicas)
|
|
.Select(p => p.PeerId)
|
|
.ToList();
|
|
|
|
// 7. Return RaftGroup with selected peer IDs.
|
|
return new RaftGroup
|
|
{
|
|
Name = groupName,
|
|
Peers = selected,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Describes a peer node available for placement consideration.
|
|
/// Go reference: jetstream_cluster.go peerInfo — peer.id, peer.offline, peer.storage.
|
|
/// </summary>
|
|
public sealed class PeerInfo
|
|
{
|
|
public required string PeerId { get; init; }
|
|
public string Cluster { get; set; } = string.Empty;
|
|
public HashSet<string> Tags { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
|
public bool Available { get; set; } = true;
|
|
public long AvailableStorage { get; set; } = long.MaxValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Placement policy specifying cluster affinity and tag constraints.
|
|
/// Go reference: jetstream_cluster.go Placement struct — cluster, tags.
|
|
/// </summary>
|
|
public sealed class PlacementPolicy
|
|
{
|
|
public string? Cluster { get; set; }
|
|
public HashSet<string>? Tags { get; set; }
|
|
public HashSet<string>? ExcludeTags { get; set; }
|
|
}
|