ec4c8fab87
Move configuration options from Core/DataAccess/DataSync/ExcelIO to dedicated Options folders within each project for better organization. Update all references and tests accordingly.
161 lines
5.7 KiB
C#
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
|
|
}
|
|
}
|
|
}
|