Files
jdescopingtool/NEW/src/JdeScoping.DataSync/DataSyncService.cs
T
Joseph Doherty ec4c8fab87 refactor: relocate options classes to dedicated Options folders
Move configuration options from Core/DataAccess/DataSync/ExcelIO to
dedicated Options folders within each project for better organization.
Update all references and tests accordingly.
2026-01-03 08:55:08 -05:00

161 lines
5.7 KiB
C#

using JdeScoping.DataSync.Options;
using JdeScoping.DataSync.Contracts;
using JdeScoping.DataSync.Telemetry;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace JdeScoping.DataSync;
/// <summary>
/// Background service that orchestrates data synchronization from JDE/CMS to SQL Server cache.
/// </summary>
public class DataSyncService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IOptions<DataSyncOptions> _options;
private readonly ILogger<DataSyncService> _logger;
private readonly DataSyncMetrics _metrics;
private DateTime _lastPurgeCheck = DateTime.MinValue;
private readonly TimeSpan _purgeCheckInterval = TimeSpan.FromHours(24);
/// <summary>
/// Initializes a new instance of the <see cref="DataSyncService"/> class.
/// </summary>
public DataSyncService(
IServiceScopeFactory scopeFactory,
IOptions<DataSyncOptions> options,
ILogger<DataSyncService> logger,
DataSyncMetrics metrics)
{
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
}
/// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!_options.Value.Enabled)
{
_logger.LogInformation("DataSyncService is disabled via configuration");
return;
}
_logger.LogInformation(
"DataSyncService starting with CheckInterval={CheckInterval}, MaxDegreeOfParallelism={MaxDegreeOfParallelism}",
_options.Value.CheckInterval,
_options.Value.MaxDegreeOfParallelism);
// Startup: close any interrupted syncs from prior runs
await CloseOpenUpdateEntriesAsync(stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
// Create scope for this sync cycle
await using var scope = _scopeFactory.CreateAsyncScope();
var orchestrator = scope.ServiceProvider.GetRequiredService<ISyncOrchestrator>();
// Check schedules and execute pending syncs
await orchestrator.ExecutePendingSyncsAsync(stoppingToken);
// Periodic purge of old DataUpdate records
await PurgeUpdateEntriesAsync(scope, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Graceful shutdown
_logger.LogInformation("DataSyncService stopping gracefully");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in sync cycle");
_metrics.RecordCycleError();
}
// Wait before next check
try
{
await Task.Delay(_options.Value.CheckInterval, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
}
_logger.LogInformation("DataSyncService stopped");
}
/// <summary>
/// Closes any open update entries from interrupted prior runs.
/// </summary>
private async Task CloseOpenUpdateEntriesAsync(CancellationToken cancellationToken)
{
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var repository = scope.ServiceProvider.GetRequiredService<IDataUpdateRepository>();
var closedCount = await repository.CloseOpenUpdateEntriesAsync(cancellationToken);
if (closedCount > 0)
{
_logger.LogWarning(
"Closed {Count} interrupted update entries from prior runs",
closedCount);
}
else
{
_logger.LogDebug("No interrupted update entries found");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to close open update entries at startup");
// Continue starting - this is not fatal
}
}
/// <summary>
/// Purges old DataUpdate records periodically.
/// </summary>
private async Task PurgeUpdateEntriesAsync(AsyncServiceScope scope, CancellationToken cancellationToken)
{
if (DateTime.UtcNow - _lastPurgeCheck < _purgeCheckInterval)
{
return;
}
_lastPurgeCheck = DateTime.UtcNow;
try
{
var repository = scope.ServiceProvider.GetRequiredService<IDataUpdateRepository>();
var purgedCount = await repository.PurgeOldEntriesAsync(
_options.Value.PurgeRetentionDays,
cancellationToken);
if (purgedCount > 0)
{
_logger.LogInformation(
"Purged {Count} DataUpdate records older than {Days} days",
purgedCount,
_options.Value.PurgeRetentionDays);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to purge old update entries");
// Continue - this is not fatal
}
}
}