familynido/src/FamilyNido.Api/Features/Calendar/CalendarSyncBackgroundService.cs
Pablo Fernández a308041d59 Initial commit
FamilyNido — a self-hosted PWA for a single household: shared calendar,
chores, meals, school agenda, health records and a family wall. One
instance per family, deployable with `docker compose` on any home
server.

Stack: .NET 10 (ASP.NET Core Minimal APIs) + EF Core 10 + PostgreSQL 16
on the backend, Angular 21 (standalone, signals, zoneless) + Tailwind
CSS v4 on the frontend, SignalR for realtime, optional OIDC alongside
local credentials, integration via a versioned `/api/v1/**` public API.

See README.md for the module overview and how to deploy.
2026-05-13 00:23:14 +02:00

81 lines
2.5 KiB
C#

using FamilyNido.Api.Options;
using Microsoft.Extensions.Options;
namespace FamilyNido.Api.Features.Calendar;
/// <summary>
/// Hosted service that runs <see cref="CalendarSynchronizer.SyncAllAsync"/> on a
/// fixed cadence (<see cref="CalendarOptions.SyncInterval"/>). Sleeps with a
/// <see cref="PeriodicTimer"/> so the loop wakes up promptly on shutdown.
/// </summary>
public sealed class CalendarSyncBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IOptionsMonitor<CalendarOptions> _options;
private readonly ILogger<CalendarSyncBackgroundService> _logger;
/// <summary>Primary constructor.</summary>
public CalendarSyncBackgroundService(
IServiceScopeFactory scopeFactory,
IOptionsMonitor<CalendarOptions> options,
ILogger<CalendarSyncBackgroundService> logger)
{
_scopeFactory = scopeFactory;
_options = options;
_logger = logger;
}
/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var initialDelay = TimeSpan.FromSeconds(30);
try
{
// Don't pile work onto a cold-starting app. A short head-start means the API is
// already serving traffic when the first sync runs.
await Task.Delay(initialDelay, stoppingToken);
}
catch (OperationCanceledException)
{
return;
}
while (!stoppingToken.IsCancellationRequested)
{
await SyncOnceAsync(stoppingToken);
var interval = _options.CurrentValue.SyncInterval;
if (interval <= TimeSpan.Zero)
{
interval = TimeSpan.FromMinutes(15);
}
try
{
await Task.Delay(interval, stoppingToken);
}
catch (OperationCanceledException)
{
return;
}
}
}
private async Task SyncOnceAsync(CancellationToken stoppingToken)
{
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var synchronizer = scope.ServiceProvider.GetRequiredService<CalendarSynchronizer>();
await synchronizer.SyncAllAsync(stoppingToken);
}
catch (OperationCanceledException)
{
// Shutting down; do not log.
}
catch (Exception ex)
{
_logger.LogError(ex, "Calendar sync iteration failed at the top level.");
}
}
}