105b3c59d7
PR Checks / test-and-build (pull_request) Successful in 8m34s
- Install wget in Web Dockerfile for compose healthcheck - Ensure HttpListener response is always closed in BotHealthCheckHostedService - Use ephemeral port in Bot health check test to avoid port conflicts - Rename NpgsqlHealthCheck test to reflect actual behavior Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
102 lines
3.0 KiB
C#
102 lines
3.0 KiB
C#
using System.Net;
|
|
|
|
namespace GmRelay.Bot.Infrastructure.Health;
|
|
|
|
public sealed class BotHealthCheckHostedService : IHostedService
|
|
{
|
|
private readonly ILogger<BotHealthCheckHostedService> _logger;
|
|
private readonly string _prefix;
|
|
private HttpListener? _listener;
|
|
private CancellationTokenSource? _cts;
|
|
private Task? _listenerTask;
|
|
|
|
public BotHealthCheckHostedService(
|
|
ILogger<BotHealthCheckHostedService> logger,
|
|
IConfiguration configuration)
|
|
{
|
|
_logger = logger;
|
|
_prefix = configuration.GetValue("HealthCheck:Prefix", "http://+:8081/")!;
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
_cts = new CancellationTokenSource();
|
|
_listener = new HttpListener();
|
|
_listener.Prefixes.Add(_prefix);
|
|
_listener.Start();
|
|
|
|
_logger.LogInformation("Health check server started on {Prefix}", _prefix);
|
|
|
|
_listenerTask = Task.Run(async () => await ListenAsync(_cts.Token), cancellationToken);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
_cts?.Cancel();
|
|
_listener?.Stop();
|
|
|
|
if (_listenerTask != null)
|
|
{
|
|
await Task.WhenAny(_listenerTask, Task.Delay(TimeSpan.FromSeconds(5), cancellationToken));
|
|
}
|
|
|
|
_listener?.Close();
|
|
_logger.LogInformation("Health check server stopped");
|
|
}
|
|
|
|
private async Task ListenAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (_listener?.IsListening == true && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var context = await _listener.GetContextAsync();
|
|
_ = Task.Run(() => HandleRequestAsync(context), cancellationToken);
|
|
}
|
|
catch (HttpListenerException) when (cancellationToken.IsCancellationRequested)
|
|
{
|
|
break;
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error in health check listener");
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task HandleRequestAsync(HttpListenerContext context)
|
|
{
|
|
var response = context.Response;
|
|
try
|
|
{
|
|
var request = context.Request;
|
|
|
|
if (request.Url?.AbsolutePath == "/health")
|
|
{
|
|
response.StatusCode = (int)HttpStatusCode.OK;
|
|
response.ContentType = "application/json";
|
|
var body = "{\"status\":\"healthy\"}"u8.ToArray();
|
|
await response.OutputStream.WriteAsync(body);
|
|
}
|
|
else
|
|
{
|
|
response.StatusCode = (int)HttpStatusCode.NotFound;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error handling health check request");
|
|
}
|
|
finally
|
|
{
|
|
response.Close();
|
|
}
|
|
}
|
|
}
|