Files
jdescopingtool/NEW/src/JdeScoping.DataSync/Services/ScheduleChecker.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

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}";
}
}