chore: initial project import
This commit is contained in:
commit
3fc4e5ca53
347 files changed
+56172
No files matched your search
@@ -0,0 +1,49 @@
|
||||
using Fleet.Server;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Monitor.Backend.Gateway;
|
||||
|
||||
/// <summary>
|
||||
/// 宿主开始停止时先关闭所有 Agent 的长连接(Close 握手,不发 revoked:Agent 会重连)。
|
||||
/// 否则 Kestrel 的有序停止要等满 30 秒的超时,才会放弃这些永远不会自己结束的 WebSocket。
|
||||
/// </summary>
|
||||
public sealed class ConnectionShutdown : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private Task? _closing;
|
||||
|
||||
public ConnectionShutdown(IServiceProvider services, IHostApplicationLifetime lifetime)
|
||||
{
|
||||
_services = services;
|
||||
_lifetime = lifetime;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// ApplicationStopping 在有序停止的最开始触发,早于 Kestrel 等待连接结束:这里不阻塞,关闭在后台进行,StopAsync 再等它做完
|
||||
_lifetime.ApplicationStopping.Register(() => _closing = CloseAsync());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_closing is { } closing)
|
||||
{
|
||||
await closing.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CloseAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _services.GetRequiredService<IFleetConnections>().CloseAllAsync("server shutting down");
|
||||
}
|
||||
catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException)
|
||||
{
|
||||
// 停止过程中,服务已经在释放:Kestrel 的超时兜底
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Fleet.Core;
|
||||
using Fleet.Server;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Monitor.Backend.Application;
|
||||
using Monitor.Protocol;
|
||||
|
||||
namespace Monitor.Backend.Gateway;
|
||||
|
||||
/// <summary>
|
||||
/// Application 通往 Agent 连接的端口的实现,建立在 Fleet.Server.Link 的在线表之上。
|
||||
/// Fleet 的连接宿主依赖节点事件处理器,而事件处理器依赖 Application,Application 又依赖这个端口:
|
||||
/// 这里在调用时才解析 Fleet 的服务,打断构造期的循环依赖。
|
||||
/// </summary>
|
||||
public sealed class FleetGateway : IAgentGateway
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public FleetGateway(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
private IFleetConnections Connections => _services.GetRequiredService<IFleetConnections>();
|
||||
|
||||
private IStateNotifier Notifier => _services.GetRequiredService<IStateNotifier>();
|
||||
|
||||
public IReadOnlyCollection<NodeId> OnlineAgents => Connections.OnlineNodes;
|
||||
|
||||
public bool IsOnline(NodeId agent) => Connections.IsOnline(agent);
|
||||
|
||||
public void NotifyTaskSetChanged(NodeId agent) => Notifier.NotifyChanged(agent);
|
||||
|
||||
public void NotifyAllTaskSetsChanged() => Notifier.NotifyAllChanged();
|
||||
|
||||
public async ValueTask NotifyProbeAgentsChangedAsync(ProbeAgentsResult agents, CancellationToken ct)
|
||||
{
|
||||
IFleetConnections connections = Connections;
|
||||
foreach (NodeId node in connections.OnlineNodes)
|
||||
{
|
||||
await connections.NotifyAsync(node, MonitorMethods.ProbeAgentsChanged, agents, MonitorJsonContext.Default.ProbeAgentsResult, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<bool>> RunTestAsync(NodeId agent, TestRunParams run, CancellationToken ct)
|
||||
{
|
||||
Result<TestRunResult> result = await Connections.RequestAsync(
|
||||
agent, MonitorMethods.TestRun, run, MonitorJsonContext.Default.TestRunParams, MonitorJsonContext.Default.TestRunResult, ct);
|
||||
if (!result.TryGet(out TestRunResult? accepted, out FleetError? error))
|
||||
{
|
||||
return Result<bool>.Fail(error.Code == FleetErrors.NodeOffline ? MonitorErrors.AgentOffline : error.Code, error.Message);
|
||||
}
|
||||
|
||||
return accepted.Accepted ? Result<bool>.Ok(true) : Result<bool>.Fail(MonitorErrors.TestBusy, "the agent did not accept the test");
|
||||
}
|
||||
|
||||
public async ValueTask CancelTestAsync(NodeId agent, TestId test, CancellationToken ct) =>
|
||||
await Connections.RequestAsync(
|
||||
agent, MonitorMethods.TestCancel, new TestCancelParams { TestId = test }, MonitorJsonContext.Default.TestCancelParams, MonitorJsonContext.Default.TestRunResult, ct);
|
||||
|
||||
public async ValueTask<Result<bool>> DisconnectProbeAsync(NodeId agent, ProbeId probe, CancellationToken ct)
|
||||
{
|
||||
Result<TestRunResult> result = await Connections.RequestAsync(
|
||||
agent, MonitorMethods.ProbeDisconnect, new ProbeDisconnectParams { ProbeId = probe }, MonitorJsonContext.Default.ProbeDisconnectParams, MonitorJsonContext.Default.TestRunResult, ct);
|
||||
if (!result.TryGet(out TestRunResult? done, out FleetError? error))
|
||||
{
|
||||
return Result<bool>.Fail(error.Code == FleetErrors.NodeOffline ? MonitorErrors.AgentOffline : error.Code, error.Message);
|
||||
}
|
||||
|
||||
return Result<bool>.Ok(done.Accepted);
|
||||
}
|
||||
|
||||
public ValueTask DisconnectAsync(NodeId agent, string reason, CancellationToken ct) => Connections.DisconnectAsync(agent, reason, ct);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Fleet.Core;
|
||||
using Fleet.Rpc;
|
||||
using Fleet.Server;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Monitor.Backend.Application;
|
||||
using Monitor.Protocol;
|
||||
|
||||
namespace Monitor.Backend.Gateway;
|
||||
|
||||
public static class GatewayServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 在 Fleet.Server 之上接入 Monitor:期望状态(TaskSet)、RawChunk 收件箱、<c>monitor.*</c> 方法、注册挂钩和节点事件。
|
||||
/// 需要事先注册 <see cref="IFleetStore"/> 和 <see cref="TimeProvider"/>,并在 <c>AddMonitorApplication</c> 之后调用。
|
||||
/// </summary>
|
||||
public static IServiceCollection AddMonitorGateway(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IAgentGateway, FleetGateway>();
|
||||
services.AddHostedService<ConnectionShutdown>();
|
||||
services.AddFleetEnrollment();
|
||||
services.AddFleetServer(options => options.NodeIdPrefix = AgentIds.Prefix)
|
||||
.AddState<TaskSetStateProvider>()
|
||||
.AddInbox<RawChunkInbox>()
|
||||
.MapMethods(MapMonitorMethods);
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void MapMonitorMethods(RpcRouter<NodeCallContext> router, IServiceProvider services)
|
||||
{
|
||||
router
|
||||
.OnNotify(
|
||||
MonitorMethods.Event,
|
||||
MonitorJsonContext.Default.AgentEventParams,
|
||||
(context, parameters, ct) => services.GetRequiredService<AgentEventService>().OnEventAsync(context.Node.Id, parameters, ct))
|
||||
.OnNotify(
|
||||
MonitorMethods.Host,
|
||||
MonitorJsonContext.Default.HostSnapshot,
|
||||
(context, snapshot, ct) => services.GetRequiredService<HostStatusService>().OnHostAsync(context.Node.Id, snapshot, ct))
|
||||
.OnNotify(
|
||||
MonitorMethods.TestStatus,
|
||||
MonitorJsonContext.Default.TestStatusParams,
|
||||
(context, parameters, ct) => services.GetRequiredService<TestService>().OnStatusAsync(context.Node.Id, parameters, ct))
|
||||
.On(
|
||||
MonitorMethods.ProbeAgents,
|
||||
MonitorJsonContext.Default.ProbeAgentsParams,
|
||||
MonitorJsonContext.Default.ProbeAgentsResult,
|
||||
async (context, parameters, ct) => Result<ProbeAgentsResult>.Ok(await services.GetRequiredService<AgentService>().ProbeAgentsAsync(ct)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Monitor.Backend.Application\Monitor.Backend.Application.csproj" />
|
||||
<ProjectReference Include="..\..\fleet\src\Fleet.Server.Link\Fleet.Server.Link.csproj" />
|
||||
<ProjectReference Include="..\..\fleet\src\Fleet.Server.Enrollment\Fleet.Server.Enrollment.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,28 @@
|
||||
using Fleet.Server;
|
||||
using Monitor.Backend.Application;
|
||||
using Monitor.Protocol;
|
||||
|
||||
namespace Monitor.Backend.Gateway;
|
||||
|
||||
/// <summary>
|
||||
/// RawChunk 收件箱(二进制通道 16):持久化之后才返回,Fleet 随后才回复 ACK,按 chunk_id 去重。
|
||||
/// 永远不可能被接受的块(损坏、冒名、未知 Kind)也确认掉并记录原因,否则节点会无限重发。
|
||||
/// </summary>
|
||||
public sealed class RawChunkInbox : IInbox
|
||||
{
|
||||
private readonly IngestService _ingest;
|
||||
|
||||
public RawChunkInbox(IngestService ingest)
|
||||
{
|
||||
_ingest = ingest;
|
||||
}
|
||||
|
||||
public byte Channel => MonitorChannels.RawChunk;
|
||||
|
||||
public async ValueTask<InboxOutcome> HandleAsync(NodeRecord node, Guid msgId, ReadOnlyMemory<byte> payload, CancellationToken ct) =>
|
||||
await _ingest.IngestAsync(node.Id, msgId, payload, ct) switch
|
||||
{
|
||||
IngestOutcome.Stored => InboxOutcome.Stored,
|
||||
_ => InboxOutcome.Duplicate,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Fleet.Rpc;
|
||||
using Fleet.Server;
|
||||
using Monitor.Backend.Application;
|
||||
using Monitor.Protocol;
|
||||
|
||||
namespace Monitor.Backend.Gateway;
|
||||
|
||||
/// <summary>
|
||||
/// 期望状态:为每个 Agent 下发完整的 <see cref="TaskSet"/> 快照(有效任务集 + Probe 公钥)。
|
||||
/// 节点应用之后回传 <c>applied_version</c> 和 <c>rejected[]</c>(<c>unsupported</c>、<c>budget_exceeded</c>、<c>policy_denied</c>),面板上可以看到。
|
||||
/// </summary>
|
||||
public sealed class TaskSetStateProvider : IStateProvider
|
||||
{
|
||||
private readonly TaskPlanner _planner;
|
||||
private readonly IAgentStore _agents;
|
||||
|
||||
public TaskSetStateProvider(TaskPlanner planner, IAgentStore agents)
|
||||
{
|
||||
_planner = planner;
|
||||
_agents = agents;
|
||||
}
|
||||
|
||||
public string Type => TaskSet.StateType;
|
||||
|
||||
public async ValueTask<StateDocument?> GetAsync(NodeRecord node, CancellationToken ct)
|
||||
{
|
||||
DesiredTaskSet? desired = await _planner.GetDesiredAsync(node.Id, ct);
|
||||
return desired is null ? null : new StateDocument(desired.Version, JsonSerializer.SerializeToElement(desired.Document, MonitorJsonContext.Default.TaskSet));
|
||||
}
|
||||
|
||||
public async ValueTask OnAppliedAsync(NodeRecord node, long version, StateSyncResult result, CancellationToken ct)
|
||||
{
|
||||
string? rejected = result.Rejected.Count == 0 ? null : JsonSerializer.Serialize(result.Rejected, GatewayJson.Default.ListRejectedItem);
|
||||
await _agents.SetAppliedTaskStateAsync(node.Id, result.AppliedVersion, rejected, ct);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(List<RejectedItem>))]
|
||||
internal sealed partial class GatewayJson : JsonSerializerContext;
|
||||
Reference in new issue
Block a user