Repository navigation
Expand file tree
/
Copy pathNodeOptions.cs
More file actions
176 lines (165 loc) · 10.1 KB
/
Copy pathNodeOptions.cs
File metadata and controls
176 lines (165 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
using System.Net;
using KeyLoad.Orleans;
using KeyLoad.Replication;
using KeyLoad.Server.Features.ClusterRouting;
namespace KeyLoad.Server;
/// <summary>Fixed cluster configuration and private physical node settings.</summary>
[ConfigurationOptions]
internal sealed record NodeOptions
{
/// <summary>Directory owned exclusively by one process, independent of grain placement.</summary>
public string DataDirectory { get; init; } = NodeDefaults.DataDirectory;
/// <summary>Stable configured voter identity and authenticated discovery HTTP origin.</summary>
public string PublicEndpoint { get; init; } = NodeDefaults.PublicEndpoint;
/// <summary>Fixed voter origins including the local origin; production requires an odd group of at least three.</summary>
public IReadOnlyList<string> Peers { get; init; } = [];
/// <summary>Trusted startup opt-in for isolated benchmark fixed groups of one, two or three voters.</summary>
public bool BenchmarkTopology { get; init; }
/// <summary>Orleans cluster identity, shared by every voter.</summary>
public string ClusterId { get; init; } = NodeDefaults.ClusterId;
/// <summary>Independent physical shard identity shared by every configured voter and stable across restart.</summary>
public Guid PhysicalShardId { get; init; }
/// <summary>Database incarnation, stable across process restarts and snapshot catch-up.</summary>
public Guid Incarnation { get; init; }
/// <summary>Base64 database signing credential; stored only in the private node catalog.</summary>
public string SigningKey { get; init; } = string.Empty;
/// <summary>Base64 peer HMAC credential; public callers cannot send replica control requests.</summary>
public string PeerSecret { get; init; } = string.Empty;
/// <summary>Initial root credential, used only when canonical storage is fresh.</summary>
public string AdminKey { get; init; } = string.Empty;
/// <summary>Native Orleans silo port; no public Orleans gateway is exposed.</summary>
public int SiloPort { get; init; } = NodeDefaults.SiloPort;
/// <summary>Routable silo IP or DNS name resolved on process startup.</summary>
public string SiloAddress { get; init; } = NodeDefaults.SiloAddress;
/// <summary>Permits plaintext discovery/public HTTP on explicit loopback development origins.</summary>
public bool AllowLoopbackHttp { get; init; }
/// <summary>Permits HTTP on the explicitly isolated Docker/Aspire development network.</summary>
public bool AllowPrivateNetworkHttp { get; init; }
/// <summary>Committed entries between canonical checkpoints.</summary>
public int SnapshotThreshold { get; init; } = NodeDefaults.SnapshotThreshold;
/// <summary>Lower randomized election bound, longer than an individual peer RPC.</summary>
public int LowerElectionTimeoutMilliseconds { get; init; } = NodeDefaults.LowerElectionMilliseconds;
/// <summary>Upper randomized election bound.</summary>
public int UpperElectionTimeoutMilliseconds { get; init; } = NodeDefaults.UpperElectionMilliseconds;
/// <summary>Overall Orleans peer RPC deadline, including bounded generation rediscovery.</summary>
public int PeerRpcTimeoutMilliseconds { get; init; } = NodeDefaults.RpcMilliseconds;
/// <summary>Data and reserved control admission limits.</summary>
public CommandAdmissionLimits CommandAdmission { get; init; } = new();
/// <summary>Public HTTP request body and concurrency limits.</summary>
public HttpAdmissionLimits HttpAdmission { get; init; } = new();
/// <summary>Independent MCP body, native serialization and retained-output byte pools.</summary>
public McpMemoryLimits McpMemory { get; init; } = new();
/// <summary>Independent bounded authenticated nonce pools per fixed voter.</summary>
public ReplicaReplayLimits ReplayAdmission { get; init; } = new();
internal RequestCqrsProbeOptions RequestCqrsProbe { get; set; } = new(false, null, string.Empty);
/// <summary>Explicit native membership provider mode; local preserves the ordinary RF3 topology.</summary>
public MembershipAuthoritySettings MembershipAuthority { get; init; } = new();
/// <summary>Rejects invalid identity, timing, transport and admission settings before opening files.</summary>
public void Validate()
{
ArgumentNullException.ThrowIfNull(CommandAdmission);
ArgumentNullException.ThrowIfNull(HttpAdmission);
ArgumentNullException.ThrowIfNull(ReplayAdmission);
ArgumentNullException.ThrowIfNull(McpMemory);
ArgumentNullException.ThrowIfNull(MembershipAuthority);
CommandAdmission.Validate();
HttpAdmission.Validate();
McpMemory.Validate();
ValidatePeers();
if (PhysicalShardId == Guid.Empty || Incarnation == Guid.Empty
|| SecretLength(SigningKey) != ReplicaTransportProtocol.SecretBytes
|| SecretLength(PeerSecret) != ReplicaTransportProtocol.SecretBytes
|| AdminKey is null || AdminKey.Length < NodeDefaults.MinimumAdminCharacters
|| !AdminKey.StartsWith(NodeDefaults.AdminPrefix, StringComparison.Ordinal)
|| SiloPort is <= IPEndPoint.MinPort or > IPEndPoint.MaxPort)
{
throw new InvalidOperationException(NodeDefaults.InvalidIdentity);
}
var configuration = CreateReplicaConfiguration(Path.GetFullPath(DataDirectory));
configuration.Validate();
MembershipAuthoritySettingsValidator.Validate(MembershipAuthority, this);
RequestCqrsProbeOptionsReader.Validate(RequestCqrsProbe, configuration, AllowPrivateNetworkHttp);
}
private void ValidatePeers()
{
const int PeersCountValidationBoundary = 2;
const int EmptyPeersCount = 0;
if (Peers is null || (BenchmarkTopology
? Peers.Count is < NodeDefaults.MinimumBenchmarkVoters or > NodeDefaults.MaximumBenchmarkVoters
: Peers.Count < NodeDefaults.MinimumVoters || Peers.Count % PeersCountValidationBoundary == EmptyPeersCount)
|| Peers.Distinct(StringComparer.Ordinal).Count() != Peers.Count
|| !Peers.Contains(PublicEndpoint, StringComparer.Ordinal)
|| string.IsNullOrWhiteSpace(DataDirectory) || string.IsNullOrWhiteSpace(SiloAddress))
{
throw new InvalidOperationException(NodeDefaults.InvalidPeers);
}
foreach (var peer in Peers)
{
if (!Uri.TryCreate(peer, UriKind.Absolute, out var uri) || !ValidOrigin(uri))
{
throw new InvalidOperationException(NodeDefaults.InvalidPeers);
}
}
if (IPAddress.TryParse(SiloAddress, out var address)
&& (address.Equals(IPAddress.Any) || address.Equals(IPAddress.IPv6Any)
|| Peers.Any(peer => !new Uri(peer).IsLoopback) && IPAddress.IsLoopback(address)))
{
throw new InvalidOperationException(NodeDefaults.InvalidAddress);
}
}
private bool ValidOrigin(Uri uri) => (uri.Scheme == Uri.UriSchemeHttps
|| uri.Scheme == Uri.UriSchemeHttp && (AllowPrivateNetworkHttp || AllowLoopbackHttp && uri.IsLoopback))
&& string.IsNullOrEmpty(uri.UserInfo) && string.IsNullOrEmpty(uri.Query) && string.IsNullOrEmpty(uri.Fragment)
&& uri.AbsolutePath == NodeDefaults.OriginPath;
private static int SecretLength(string? encoded)
{
const int SecretLengthEmptyResult = 0;
const int SecretLengthAbsentCount = 0;
if (encoded is null)
{ return SecretLengthEmptyResult; }
Span<byte> bytes = stackalloc byte[ReplicaTransportProtocol.SecretBytes];
return Convert.TryFromBase64String(encoded, bytes, out var count) ? count : SecretLengthAbsentCount;
}
/// <summary>Creates the immutable replica scope without storage or an Orleans client.</summary>
/// <param name="directory">The full physical node directory.</param>
[ConfigurationBinding]
public ReplicaConfiguration CreateReplicaConfiguration(string directory) => new(PublicEndpoint, [.. Peers], directory, Incarnation)
{
BenchmarkTopology = BenchmarkTopology,
SnapshotThreshold = SnapshotThreshold,
LowerElectionTimeout = TimeSpan.FromMilliseconds(LowerElectionTimeoutMilliseconds),
UpperElectionTimeout = TimeSpan.FromMilliseconds(UpperElectionTimeoutMilliseconds),
RpcTimeout = TimeSpan.FromMilliseconds(PeerRpcTimeoutMilliseconds)
};
/// <summary>Creates fixed discovery endpoints and bounded HMAC replay limits.</summary>
/// <param name="connectTimeout">The centrally validated peer-discovery connection bound.</param>
[ConfigurationBinding]
public ReplicaPeerOptions CreatePeerOptions(TimeSpan connectTimeout) => new(Peers.ToDictionary(peer => peer, peer => new Uri(peer), StringComparer.Ordinal),
Convert.FromBase64String(PeerSecret), ClusterId)
{
ConnectTimeout = connectTimeout,
ReplayLimits = ReplayAdmission,
MaxControlPayloadBytes = CommandAdmission.MaxControlPayloadBytes
};
private static class NodeDefaults
{
internal const string DataDirectory = "data/node";
internal const string PublicEndpoint = "http://127.0.0.1:5100";
internal const string ClusterId = "keyload";
internal const string SiloAddress = "127.0.0.1";
internal const string AdminPrefix = "root.";
internal const string OriginPath = "/";
internal const string InvalidIdentity = "Cluster identity, credentials or silo port are invalid.";
internal const string InvalidPeers = "Cluster origins require distinct fixed voters, private development HTTP or HTTPS, and a local member.";
internal const string InvalidAddress = "A remote voter requires a routable advertised silo address.";
internal const int MinimumVoters = 3;
internal const int MinimumBenchmarkVoters = 1;
internal const int MaximumBenchmarkVoters = 3;
internal const int SiloPort = 11_111;
internal const int SnapshotThreshold = 1_024;
internal const int LowerElectionMilliseconds = 4_000;
internal const int UpperElectionMilliseconds = 8_000;
internal const int RpcMilliseconds = 2_000;
internal const int MinimumAdminCharacters = 32;
}
}