using Grpc.Core;
using Grpc.Health.V1;
using Grpc.Net.Client;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
using Deal.Infrastructure.Integrations.Extensions;
using Deal.Infrastructure.Integrations.Models;
using Deal.Infrastructure.Integrations.Options;
namespace Deal.Infrastructure.Integrations.Services;
///
/// Health-проба grpc.health.v1 автономных сервисов (ml/ai/telegram) для операторского health
/// (план Task 10: GET /api/operator/health, Ruling 3/6/9).
///
///
/// Каждый вызов строит свой короткоживущий канал к endpoint сервиса (dev — без TLS, Ruling 2;
/// mTLS (Ruling 6, Task 13): при включённом флаге канал подписывает запрос клиентским сертификатом и
/// проверяет CA сервера — сертификаты передаются в конструктор) и спрашивает
/// Health.Check("") с дедлайном 3 с — health не должен висеть дольше таймаута. Классификация: ответ
/// SERVING → Reachable+Serving; ответ с иным статусом → Reachable без
/// Serving; таймаут/нет соединения (Unavailable/DeadlineExceeded, HTTP-транспорт) и сервис без health-контракта
/// (Unimplemented) → .
///
public sealed class ServiceHealthProbe
{
///
/// Дедлайн health-RPC, секунд (Ruling 3/9: операторский health отвечает за ~3 с на сервис).
///
public const int HealthTimeoutSeconds = 3;
private readonly MtlsCertificates? _mtlsCertificates;
///
/// Создаёт пробу; mTLS-каналы — при переданных сертификатах (иначе plaintext, dev).
///
/// Сертификаты mTLS (Ruling 6, Task 13): null — plaintext-канал.
public ServiceHealthProbe(MtlsCertificates? mtlsCertificates = null)
{
_mtlsCertificates = mtlsCertificates;
}
///
/// Проверяет health-контракт gRPC-сервиса по базовому адресу (grpc.health.v1, сервис "").
///
/// Базовый адрес сервиса (http://host:port; пустой/пробельный — ошибка аргумента).
/// Токен отмены вызывающего.
/// Результат пробы (см. ).
public async Task ProbeAsync(string endpoint, CancellationToken ct)
{
ArgumentException.ThrowIfNullOrWhiteSpace(endpoint);
using SocketsHttpHandler? handler = _mtlsCertificates?.CreateClientHttpHandler();
GrpcChannelOptions channelOptions = new() { MaxRetryAttempts = 0, HttpHandler = handler };
using var channel = GrpcChannel.ForAddress(endpoint, channelOptions);
var client = new Health.HealthClient(channel);
try
{
HealthCheckResponse response = await client.CheckAsync(
new HealthCheckRequest { Service = string.Empty },
deadline: DateTime.UtcNow.AddSeconds(HealthTimeoutSeconds),
cancellationToken: ct);
return new ServiceHealthResult(
Reachable: true,
Serving: response.Status == HealthCheckResponse.Types.ServingStatus.Serving);
}
catch (RpcException exception) when (exception.IsCommunicationFailure())
{
return ServiceHealthResult.Unreachable;
}
catch (HttpRequestException)
{
// Ошибка транспорта HTTP/2 (DNS/соединение) — до gRPC-статуса не дошло.
return ServiceHealthResult.Unreachable;
}
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
{
// Сработал дедлайн пробы (отмена вызывающего выше пробросилась бы дальше) — сервис не ответил за 3 с.
return ServiceHealthResult.Unreachable;
}
}
}