using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; public sealed class GatewayHeartbeatService : BackgroundService { private readonly IServiceScopeFactory _scopes; private readonly IHttpClientFactory _http; private readonly CentralReachabilityService _reachability; private readonly GatewayOptions _gateway; private readonly IConfiguration _config; private readonly ILogger _logger; public GatewayHeartbeatService( IServiceScopeFactory scopes, IHttpClientFactory http, CentralReachabilityService reachability, IOptions gateway, IConfiguration config, ILogger logger) { _scopes = scopes; _http = http; _reachability = reachability; _gateway = gateway.Value; _config = config; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken ct) { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(_gateway.HeartbeatIntervalSeconds)); while (await timer.WaitForNextTickAsync(ct)) await SendHeartbeatAsync(ct); } private async Task SendHeartbeatAsync(CancellationToken ct) { await using var scope = _scopes.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); var bufferDepth = await db.BufferedSyncItems.CountAsync(b => !b.Synced, ct); var status = _reachability.IsCentralReachable ? "ONLINE" : "DEGRADED"; var req = new GatewayHeartbeatRequest(status, bufferDepth, DateTimeOffset.UtcNow); try { var client = _http.CreateClient("central"); client.BaseAddress = new Uri( scope.ServiceProvider.GetRequiredService>().Value.BaseUrl); client.DefaultRequestHeaders.Remove("X-Api-Key"); client.DefaultRequestHeaders.Remove("X-Gateway-Id"); client.DefaultRequestHeaders.Add("X-Api-Key", _config["ApiKey:Gateway"]); client.DefaultRequestHeaders.Add("X-Gateway-Id", _gateway.GatewayId.ToString()); var resp = await client.PatchAsJsonAsync( $"/api/v1/gateways/{_gateway.GatewayId}/heartbeat", req, ct); if (!resp.IsSuccessStatusCode) _logger.LogWarning("Heartbeat failed: {Status}", resp.StatusCode); } catch (Exception ex) { _logger.LogWarning(ex, "Heartbeat to central failed — gateway continues in DEGRADED mode"); } } }