chore: initial project import
This commit is contained in:
commit
26620707a0
347 files changed
+56172
No files matched your search
@@ -0,0 +1,28 @@
|
||||
namespace Fleet.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 错误码:snake_case 字符串。框架的错误码以 <c>fleet_</c> 开头,产品可以扩展自己的错误码。
|
||||
/// </summary>
|
||||
public readonly record struct ErrorCode(string Value)
|
||||
{
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
|
||||
public static class FleetErrors
|
||||
{
|
||||
public static readonly ErrorCode Unauthorized = new("fleet_unauthorized");
|
||||
public static readonly ErrorCode Forbidden = new("fleet_forbidden");
|
||||
public static readonly ErrorCode Unsupported = new("fleet_unsupported");
|
||||
public static readonly ErrorCode InvalidParams = new("fleet_invalid_params");
|
||||
public static readonly ErrorCode NotFound = new("fleet_not_found");
|
||||
public static readonly ErrorCode Internal = new("fleet_internal");
|
||||
public static readonly ErrorCode Timeout = new("fleet_timeout");
|
||||
public static readonly ErrorCode NodeOffline = new("fleet_node_offline");
|
||||
public static readonly ErrorCode NodePending = new("fleet_node_pending");
|
||||
public static readonly ErrorCode NodeDisabled = new("fleet_node_disabled");
|
||||
public static readonly ErrorCode NameConflict = new("fleet_name_conflict");
|
||||
public static readonly ErrorCode JoinTokenInvalid = new("fleet_join_token_invalid");
|
||||
public static readonly ErrorCode JoinTokenExpired = new("fleet_join_token_expired");
|
||||
public static readonly ErrorCode ProofInvalid = new("fleet_proof_invalid");
|
||||
public static readonly ErrorCode FrameTooLarge = new("fleet_frame_too_large");
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<RootNamespace>Fleet.Core</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)../../../BannedSymbols.Node.txt" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Fleet.Core;
|
||||
|
||||
public enum NodeLogLevel
|
||||
{
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// <summary>节点日志:字段名统一为 logfmt(<c>ts level msg agent probe session task result err</c>)。逐包事件不记录,只累加计数。</summary>
|
||||
public interface INodeLog
|
||||
{
|
||||
void Write(NodeLogLevel level, string message, params (string Key, object? Value)[] fields);
|
||||
}
|
||||
|
||||
public sealed class NullNodeLog : INodeLog
|
||||
{
|
||||
public static NullNodeLog Instance { get; } = new();
|
||||
|
||||
public void Write(NodeLogLevel level, string message, params (string Key, object? Value)[] fields)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static class NodeLogExtensions
|
||||
{
|
||||
public static void Info(this INodeLog log, string message, params (string Key, object? Value)[] fields) => log.Write(NodeLogLevel.Info, message, fields);
|
||||
|
||||
public static void Warn(this INodeLog log, string message, params (string Key, object? Value)[] fields) => log.Write(NodeLogLevel.Warn, message, fields);
|
||||
|
||||
public static void Error(this INodeLog log, string message, params (string Key, object? Value)[] fields) => log.Write(NodeLogLevel.Error, message, fields);
|
||||
}
|
||||
|
||||
/// <summary>指数退避(带随机抖动):1 s → 20 s(对端恢复之后最多等 20 秒;节点数量不大,重连风暴不是问题)。连接成功之后调用 <see cref="Reset"/>。</summary>
|
||||
public sealed class Backoff
|
||||
{
|
||||
private readonly TimeSpan _initial;
|
||||
private readonly TimeSpan _max;
|
||||
private int _attempt;
|
||||
|
||||
public Backoff(TimeSpan? initial = null, TimeSpan? max = null)
|
||||
{
|
||||
_initial = initial ?? TimeSpan.FromSeconds(1);
|
||||
_max = max ?? TimeSpan.FromSeconds(20);
|
||||
}
|
||||
|
||||
public TimeSpan Next()
|
||||
{
|
||||
double baseMs = Math.Min(_max.TotalMilliseconds, _initial.TotalMilliseconds * Math.Pow(2, Math.Min(_attempt, 20)));
|
||||
_attempt++;
|
||||
// 抖动:0.5 ~ 1.0 倍,避免所有节点同时重连
|
||||
double jitter = 0.5 + (RandomNumberGenerator.GetInt32(0, 1000) / 2000.0);
|
||||
return TimeSpan.FromMilliseconds(baseMs * jitter);
|
||||
}
|
||||
|
||||
public void Reset() => _attempt = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 序号重放窗口(滑动 1024 位):同一个序号只接受一次;比窗口还旧的序号视为重放。
|
||||
/// 用于数据面的 UDP 包,每个 (session, stream) 一个。
|
||||
/// </summary>
|
||||
public sealed class ReplayWindow
|
||||
{
|
||||
private const int Size = 1024;
|
||||
private readonly ulong[] _bits = new ulong[Size / 64];
|
||||
private long _highest = -1;
|
||||
|
||||
public bool TryAccept(uint seq)
|
||||
{
|
||||
if (_highest < 0)
|
||||
{
|
||||
_highest = seq;
|
||||
Set(seq);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (seq > _highest)
|
||||
{
|
||||
long shift = seq - _highest;
|
||||
if (shift >= Size)
|
||||
{
|
||||
Array.Clear(_bits);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (long s = _highest + 1; s <= seq; s++)
|
||||
{
|
||||
Clear((uint)s);
|
||||
}
|
||||
}
|
||||
|
||||
_highest = seq;
|
||||
Set(seq);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_highest - seq >= Size)
|
||||
{
|
||||
return false; // 太旧
|
||||
}
|
||||
|
||||
if (IsSet(seq))
|
||||
{
|
||||
return false; // 重复
|
||||
}
|
||||
|
||||
Set(seq);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Set(uint seq) => _bits[(int)(seq % Size) / 64] |= 1UL << (int)(seq % 64);
|
||||
|
||||
private void Clear(uint seq) => _bits[(int)(seq % Size) / 64] &= ~(1UL << (int)(seq % 64));
|
||||
|
||||
private bool IsSet(uint seq) => (_bits[(int)(seq % Size) / 64] & (1UL << (int)(seq % 64))) != 0;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace Fleet.Core;
|
||||
|
||||
public record FleetError(ErrorCode Code, string Message)
|
||||
{
|
||||
public override string ToString() => $"{Code}: {Message}";
|
||||
}
|
||||
|
||||
/// <summary>预期内的失败用结果表示,不抛异常。</summary>
|
||||
public readonly struct Result<T>
|
||||
{
|
||||
private readonly T? _value;
|
||||
private readonly FleetError? _error;
|
||||
|
||||
private Result(T? value, FleetError? error)
|
||||
{
|
||||
_value = value;
|
||||
_error = error;
|
||||
}
|
||||
|
||||
public bool IsSuccess => _error is null;
|
||||
|
||||
public T Value => _error is null ? _value! : throw new InvalidOperationException(_error.ToString());
|
||||
|
||||
public FleetError Error => _error ?? throw new InvalidOperationException("Result is successful.");
|
||||
|
||||
public static Result<T> Ok(T value) => new(value, null);
|
||||
|
||||
public static Result<T> Fail(FleetError error) => new(default, error);
|
||||
|
||||
public static Result<T> Fail(ErrorCode code, string message) => new(default, new FleetError(code, message));
|
||||
|
||||
public bool TryGet(out T value, out FleetError error)
|
||||
{
|
||||
value = _value!;
|
||||
error = _error!;
|
||||
return _error is null;
|
||||
}
|
||||
|
||||
public Result<TOut> Map<TOut>(Func<T, TOut> map) =>
|
||||
_error is null ? Result<TOut>.Ok(map(_value!)) : Result<TOut>.Fail(_error);
|
||||
|
||||
public static implicit operator Result<T>(FleetError error) => Fail(error);
|
||||
}
|
||||
|
||||
public static class Result
|
||||
{
|
||||
public static Result<T> Ok<T>(T value) => Result<T>.Ok(value);
|
||||
|
||||
public static Result<T> Fail<T>(ErrorCode code, string message) => Result<T>.Fail(code, message);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Fleet.Core;
|
||||
|
||||
/// <summary>节点 ID(带类型前缀的 ULID)。Monitor 中的 AgentId 就是 NodeId。</summary>
|
||||
[JsonConverter(typeof(NodeIdJsonConverter))]
|
||||
public readonly record struct NodeId(string Value)
|
||||
{
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
|
||||
/// <summary>强类型字符串 ID 的 JSON 转换器基类(源生成友好,不使用反射)。</summary>
|
||||
public abstract class StringIdJsonConverter<TId> : JsonConverter<TId>
|
||||
{
|
||||
protected abstract TId Create(string value);
|
||||
|
||||
protected abstract string GetValue(TId id);
|
||||
|
||||
public sealed override TId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
Create(reader.GetString() ?? throw new JsonException("id must be a string"));
|
||||
|
||||
public sealed override void Write(Utf8JsonWriter writer, TId value, JsonSerializerOptions options) =>
|
||||
writer.WriteStringValue(GetValue(value));
|
||||
|
||||
public sealed override TId ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
Create(reader.GetString() ?? throw new JsonException("id must be a string"));
|
||||
|
||||
public sealed override void WriteAsPropertyName(Utf8JsonWriter writer, TId value, JsonSerializerOptions options) =>
|
||||
writer.WritePropertyName(GetValue(value));
|
||||
}
|
||||
|
||||
public sealed class NodeIdJsonConverter : StringIdJsonConverter<NodeId>
|
||||
{
|
||||
protected override NodeId Create(string value) => new(value);
|
||||
|
||||
protected override string GetValue(NodeId id) => id.Value;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Fleet.Core;
|
||||
|
||||
/// <summary>令牌的生成与哈希:服务端只保存 SHA-256,明文只在创建时返回一次。</summary>
|
||||
public static class TokenHash
|
||||
{
|
||||
public static byte[] Sha256(string token) => SHA256.HashData(Encoding.UTF8.GetBytes(token));
|
||||
|
||||
/// <summary>生成高熵令牌:<c>{prefix}_{43 个 base64url 字符}</c>。</summary>
|
||||
public static string NewToken(string prefix)
|
||||
{
|
||||
Span<byte> random = stackalloc byte[32];
|
||||
RandomNumberGenerator.Fill(random);
|
||||
return prefix + "_" + Base64Url.Encode(random);
|
||||
}
|
||||
|
||||
public static bool FixedTimeEquals(ReadOnlySpan<byte> a, ReadOnlySpan<byte> b) =>
|
||||
CryptographicOperations.FixedTimeEquals(a, b);
|
||||
}
|
||||
|
||||
public static class Base64Url
|
||||
{
|
||||
public static string Encode(ReadOnlySpan<byte> data) =>
|
||||
Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||
|
||||
public static bool TryDecode(string text, out byte[] data)
|
||||
{
|
||||
string s = text.Replace('-', '+').Replace('_', '/');
|
||||
s = s.PadRight(s.Length + ((4 - (s.Length % 4)) % 4), '=');
|
||||
try
|
||||
{
|
||||
data = Convert.FromBase64String(s);
|
||||
return true;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
data = [];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Fleet.Core;
|
||||
|
||||
/// <summary>26 个字符的 ULID(Crockford base32,前 48 位为毫秒时间戳),按时间可排序。</summary>
|
||||
public static class Ulid
|
||||
{
|
||||
private const string Alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||
|
||||
public static string New(TimeProvider time)
|
||||
{
|
||||
long ms = time.GetUtcNow().ToUnixTimeMilliseconds();
|
||||
Span<byte> random = stackalloc byte[10];
|
||||
RandomNumberGenerator.Fill(random);
|
||||
|
||||
Span<char> chars = stackalloc char[26];
|
||||
for (int i = 9; i >= 0; i--)
|
||||
{
|
||||
chars[i] = Alphabet[(int)(ms & 31)];
|
||||
ms >>= 5;
|
||||
}
|
||||
|
||||
// 80 位随机数 → 16 个 base32 字符
|
||||
UInt128 r = 0;
|
||||
foreach (byte b in random)
|
||||
{
|
||||
r = (r << 8) | b;
|
||||
}
|
||||
|
||||
for (int i = 25; i >= 10; i--)
|
||||
{
|
||||
chars[i] = Alphabet[(int)(r & 31)];
|
||||
r >>= 5;
|
||||
}
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
/// <summary>带类型前缀的 ID,例如 <c>agt_01J…</c>。</summary>
|
||||
public static string NewPrefixed(string prefix, TimeProvider time) => prefix + "_" + New(time);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace Fleet.Core;
|
||||
|
||||
/// <summary>LEB128 变长整数(无符号),以及 zigzag 有符号映射。</summary>
|
||||
public static class VarInt
|
||||
{
|
||||
public const int MaxBytes = 10;
|
||||
|
||||
public static ulong ZigZag(long value) => (ulong)((value << 1) ^ (value >> 63));
|
||||
|
||||
public static long UnZigZag(ulong value) => (long)(value >> 1) ^ -(long)(value & 1);
|
||||
|
||||
public static int Write(Span<byte> destination, ulong value)
|
||||
{
|
||||
int i = 0;
|
||||
while (value >= 0x80)
|
||||
{
|
||||
destination[i++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
|
||||
destination[i++] = (byte)value;
|
||||
return i;
|
||||
}
|
||||
|
||||
public static void Write(Stream destination, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[MaxBytes];
|
||||
int n = Write(buffer, value);
|
||||
destination.Write(buffer[..n]);
|
||||
}
|
||||
|
||||
/// <summary>读取失败(数据被截断或超过 10 字节)时返回 false。</summary>
|
||||
public static bool TryRead(ReadOnlySpan<byte> source, out ulong value, out int consumed)
|
||||
{
|
||||
value = 0;
|
||||
consumed = 0;
|
||||
int shift = 0;
|
||||
while (consumed < source.Length && consumed < MaxBytes)
|
||||
{
|
||||
byte b = source[consumed++];
|
||||
value |= (ulong)(b & 0x7F) << shift;
|
||||
if ((b & 0x80) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
shift += 7;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryReadUInt32LittleEndian(ReadOnlySpan<byte> source, out uint value)
|
||||
{
|
||||
if (source.Length < 4)
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(source);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user