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.
248 lines
8.2 KiB
C#
248 lines
8.2 KiB
C#
using JdeScoping.Core.Models;
|
|
using JdeScoping.Core.Models.Enums;
|
|
using JdeScoping.Core.Models.Infrastructure;
|
|
using JdeScoping.DataSync.Options;
|
|
using JdeScoping.DataSync.Contracts;
|
|
using JdeScoping.DataSync.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace JdeScoping.DataSync.Services;
|
|
|
|
/// <summary>
|
|
/// Checks schedules and determines which sync tasks need to be executed.
|
|
/// </summary>
|
|
public class ScheduleChecker : IScheduleChecker
|
|
{
|
|
private readonly IDataUpdateRepository _repository;
|
|
private readonly IOptions<DataSyncOptions> _options;
|
|
private readonly ILogger<ScheduleChecker> _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ScheduleChecker"/> class.
|
|
/// </summary>
|
|
public ScheduleChecker(
|
|
IDataUpdateRepository repository,
|
|
IOptions<DataSyncOptions> options,
|
|
ILogger<ScheduleChecker> logger)
|
|
{
|
|
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<List<DataUpdateTask>> GetPendingTasksAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var lastUpdates = await _repository.GetLastDataUpdatesAsync(cancellationToken);
|
|
var tasks = new List<DataUpdateTask>();
|
|
var now = DateTime.UtcNow;
|
|
|
|
foreach (var config in _options.Value.DataSources.Where(c => c.IsEnabled))
|
|
{
|
|
var task = CheckConfigSchedule(config, lastUpdates, now);
|
|
if (task != null)
|
|
{
|
|
tasks.Add(task);
|
|
}
|
|
}
|
|
|
|
if (tasks.Count > 0)
|
|
{
|
|
_logger.LogInformation(
|
|
"Found {Count} pending sync tasks: {Tasks}",
|
|
tasks.Count,
|
|
string.Join(", ", tasks.Select(t => $"{t.TableName}({t.UpdateType})")));
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("No pending sync tasks found");
|
|
}
|
|
|
|
return tasks;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks a single data source config and returns a task if sync is needed.
|
|
/// Priority order: Mass > Daily > Hourly
|
|
/// </summary>
|
|
private DataUpdateTask? CheckConfigSchedule(
|
|
DataSourceConfig config,
|
|
Dictionary<string, DataUpdate> lastUpdates,
|
|
DateTime now)
|
|
{
|
|
// Get last updates for each type
|
|
var massKey = GetUpdateKey(config.TableName, UpdateTypes.Mass);
|
|
var dailyKey = GetUpdateKey(config.TableName, UpdateTypes.Daily);
|
|
var hourlyKey = GetUpdateKey(config.TableName, UpdateTypes.Hourly);
|
|
|
|
lastUpdates.TryGetValue(massKey, out var lastMass);
|
|
lastUpdates.TryGetValue(dailyKey, out var lastDaily);
|
|
lastUpdates.TryGetValue(hourlyKey, out var lastHourly);
|
|
|
|
// Check Mass first (highest priority)
|
|
if (config.MassConfig.Enabled && NeedsMassSync(config, lastMass, now))
|
|
{
|
|
_logger.LogDebug(
|
|
"Mass sync needed for {Table}: last={LastSync}, interval={Interval}m",
|
|
config.TableName,
|
|
lastMass?.EndDt.ToString("o") ?? "never",
|
|
config.MassConfig.IntervalMinutes);
|
|
|
|
return CreateTask(config, UpdateTypes.Mass, null);
|
|
}
|
|
|
|
// Check Daily
|
|
if (config.DailyConfig.Enabled && NeedsDailySync(config, lastDaily, lastMass, now))
|
|
{
|
|
var minimumDt = CalculateMinimumDt(lastDaily, config.DailyConfig.IntervalMinutes);
|
|
|
|
_logger.LogDebug(
|
|
"Daily sync needed for {Table}: last={LastSync}, interval={Interval}m, minDT={MinDT}",
|
|
config.TableName,
|
|
lastDaily?.EndDt.ToString("o") ?? "never",
|
|
config.DailyConfig.IntervalMinutes,
|
|
minimumDt?.ToString("o") ?? "null");
|
|
|
|
return CreateTask(config, UpdateTypes.Daily, minimumDt);
|
|
}
|
|
|
|
// Check Hourly (uses Daily's last timestamp for MinimumDT calculation, per legacy behavior)
|
|
if (config.HourlyConfig.Enabled && NeedsHourlySync(config, lastHourly, lastDaily, lastMass, now))
|
|
{
|
|
// Use daily update timestamp for lookback, not hourly
|
|
var minimumDt = CalculateMinimumDt(lastDaily, config.DailyConfig.IntervalMinutes);
|
|
|
|
_logger.LogDebug(
|
|
"Hourly sync needed for {Table}: last={LastSync}, interval={Interval}m, minDT={MinDT}",
|
|
config.TableName,
|
|
lastHourly?.EndDt.ToString("o") ?? "never",
|
|
config.HourlyConfig.IntervalMinutes,
|
|
minimumDt?.ToString("o") ?? "null");
|
|
|
|
return CreateTask(config, UpdateTypes.Hourly, minimumDt);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a mass sync is needed.
|
|
/// </summary>
|
|
private bool NeedsMassSync(DataSourceConfig config, DataUpdate? lastMass, DateTime now)
|
|
{
|
|
// Never synced before - need mass sync
|
|
if (lastMass == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Check if successful last mass sync was within interval
|
|
if (!lastMass.WasSuccessful)
|
|
{
|
|
// Last sync failed - try again
|
|
return true;
|
|
}
|
|
|
|
var nextSyncDue = lastMass.EndDt.AddMinutes(config.MassConfig.IntervalMinutes);
|
|
return now > nextSyncDue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a daily sync is needed.
|
|
/// </summary>
|
|
private bool NeedsDailySync(DataSourceConfig config, DataUpdate? lastDaily, DataUpdate? lastMass, DateTime now)
|
|
{
|
|
// If no mass sync ever happened, we need mass first
|
|
if (lastMass == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Never done daily sync
|
|
if (lastDaily == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Check if successful last daily sync was within interval
|
|
if (!lastDaily.WasSuccessful)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var nextSyncDue = lastDaily.EndDt.AddMinutes(config.DailyConfig.IntervalMinutes);
|
|
return now > nextSyncDue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if an hourly sync is needed.
|
|
/// </summary>
|
|
private bool NeedsHourlySync(
|
|
DataSourceConfig config,
|
|
DataUpdate? lastHourly,
|
|
DataUpdate? lastDaily,
|
|
DataUpdate? lastMass,
|
|
DateTime now)
|
|
{
|
|
// If no mass sync ever happened, we need mass first
|
|
if (lastMass == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Never done hourly sync
|
|
if (lastHourly == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Check if successful last hourly sync was within interval
|
|
if (!lastHourly.WasSuccessful)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var nextSyncDue = lastHourly.EndDt.AddMinutes(config.HourlyConfig.IntervalMinutes);
|
|
return now > nextSyncDue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the MinimumDT for incremental updates using lookback multiplier.
|
|
/// </summary>
|
|
private DateTime? CalculateMinimumDt(DataUpdate? lastUpdate, int intervalMinutes)
|
|
{
|
|
if (lastUpdate == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var lookbackMinutes = _options.Value.LookbackMultiplier * intervalMinutes;
|
|
return lastUpdate.EndDt.AddMinutes(-lookbackMinutes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a data update task.
|
|
/// </summary>
|
|
private static DataUpdateTask CreateTask(DataSourceConfig config, UpdateTypes updateType, DateTime? minimumDt)
|
|
{
|
|
return new DataUpdateTask
|
|
{
|
|
TableName = config.TableName,
|
|
SourceSystem = config.SourceSystem,
|
|
SourceData = config.SourceData,
|
|
UpdateType = updateType,
|
|
MinimumDt = minimumDt,
|
|
Config = config
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the dictionary key for looking up last updates.
|
|
/// </summary>
|
|
private static string GetUpdateKey(string tableName, UpdateTypes updateType)
|
|
{
|
|
return $"{tableName}_{(int)updateType}";
|
|
}
|
|
}
|