shoc-backend/Api.SeaHavenIndustries/HostedServices/WorkOrderReconciliationHostedService.cs
Alexandre Brandizzi 6ceb274bfb
Some checks failed
Validate and deploy / Validate deployable source bundle (push) Has been cancelled
Validate and deploy / Deploy shoc-backend-dev through Terraform (push) Has been cancelled
Validate and deploy / Deploy shoc-backend-staging to Elastic Beanstalk (push) Has been cancelled
feat: add API Sentry tracing (SH-298) (#105)
* feat: add API Sentry tracing

* feat: activate Sentry deployment environments

* fix: allow Sentry-free design-time tooling

* fix: trace background jobs in Sentry

* feat(observability): identify and scrub Sentry transactions

* fix(observability): finish abandoned transactions
2026-09-04 14:11:32 -03:00

113 lines
4.4 KiB
C#

using Api.SeaHavenIndustries.Observability;
using Microsoft.Extensions.Options;
using SeaHaven.Services.Configuration;
using SeaHaven.Services.Interfaces;
using Sentry;
namespace Api.SeaHavenIndustries.HostedServices
{
public sealed class WorkOrderReconciliationHostedService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IOptionsMonitor<WorkOrderReconciliationOptions> _options;
private readonly TimeProvider _timeProvider;
private readonly ILogger<WorkOrderReconciliationHostedService> _logger;
private readonly IHub _sentryHub;
public WorkOrderReconciliationHostedService(
IServiceScopeFactory scopeFactory,
IOptionsMonitor<WorkOrderReconciliationOptions> options,
TimeProvider timeProvider,
ILogger<WorkOrderReconciliationHostedService> logger,
IHub sentryHub)
{
_scopeFactory = scopeFactory;
_options = options;
_timeProvider = timeProvider;
_logger = logger;
_sentryHub = sentryHub;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var nextScheduledAt = _timeProvider.GetUtcNow();
if (_options.CurrentValue.Enabled && _options.CurrentValue.RunOnStartup)
{
await TriggerAsync("startup", stoppingToken);
nextScheduledAt = _timeProvider.GetUtcNow().AddMinutes(_options.CurrentValue.ScheduleMinutes);
}
while (!stoppingToken.IsCancellationRequested)
{
var options = _options.CurrentValue;
if (options.Enabled)
{
var now = _timeProvider.GetUtcNow();
if (now >= nextScheduledAt)
{
await TriggerAsync("schedule", stoppingToken);
nextScheduledAt = now.AddMinutes(options.ScheduleMinutes);
}
await RunPendingAsync(stoppingToken);
}
await Task.Delay(
TimeSpan.FromSeconds(Math.Clamp(options.PollSeconds, 1, 300)),
_timeProvider,
stoppingToken);
}
}
private async Task TriggerAsync(string reason, CancellationToken cancellationToken)
{
using var transaction = SentryObservability.BeginBackgroundTransaction(
_sentryHub,
"workorders.reconciliation-trigger",
$"{nameof(WorkOrderReconciliationHostedService)}.{nameof(TriggerAsync)}",
description: reason);
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var runner = scope.ServiceProvider.GetRequiredService<IWorkOrderReconciliationRunner>();
await runner.TriggerAsync(reason, cancellationToken);
transaction.FinishOk();
}
catch (OperationCanceledException)
{
transaction.FinishCancelled();
throw;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Could not queue procurement reconciliation.");
transaction.FinishError(ex);
}
}
private async Task RunPendingAsync(CancellationToken cancellationToken)
{
using var transaction = SentryObservability.BeginBackgroundTransaction(
_sentryHub,
"workorders.reconciliation-run-pending",
$"{nameof(WorkOrderReconciliationHostedService)}.{nameof(RunPendingAsync)}");
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var runner = scope.ServiceProvider.GetRequiredService<IWorkOrderReconciliationRunner>();
await runner.RunPendingAsync(cancellationToken);
transaction.FinishOk();
}
catch (OperationCanceledException)
{
transaction.FinishCancelled();
throw;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Could not execute procurement reconciliation.");
transaction.FinishError(ex);
}
}
}
}