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; } } }