using System; using System.Threading; using System.Threading.Tasks; using Serilog; using ZB.MOM.WW.OtOpcUa.Host.Domain; namespace ZB.MOM.WW.OtOpcUa.Host.GalaxyRepository { /// /// Polls the Galaxy database for deployment changes and fires OnGalaxyChanged. (GR-003, GR-004) /// public class ChangeDetectionService : IDisposable { private static readonly ILogger Log = Serilog.Log.ForContext(); private readonly int _intervalSeconds; private readonly IGalaxyRepository _repository; private CancellationTokenSource? _cts; private Task? _pollTask; /// /// Initializes a new change detector for Galaxy deploy timestamps. /// /// The repository used to query the latest deploy timestamp. /// The polling interval, in seconds, between deploy checks. /// An optional deploy timestamp already known at service startup. public ChangeDetectionService(IGalaxyRepository repository, int intervalSeconds, DateTime? initialDeployTime = null) { _repository = repository; _intervalSeconds = intervalSeconds; LastKnownDeployTime = initialDeployTime; } /// /// Gets the last deploy timestamp observed by the polling loop. /// public DateTime? LastKnownDeployTime { get; private set; } /// /// Stops the polling loop and disposes the underlying cancellation resources. /// public void Dispose() { Stop(); _cts?.Dispose(); } /// /// Occurs when a new Galaxy deploy timestamp indicates the OPC UA address space should be rebuilt. /// public event Action? OnGalaxyChanged; /// /// Starts the background polling loop that watches for Galaxy deploy changes. /// public void Start() { if (_cts != null) Stop(); _cts = new CancellationTokenSource(); _pollTask = Task.Run(() => PollLoopAsync(_cts.Token)); Log.Information("Change detection started (interval={Interval}s)", _intervalSeconds); } /// /// Stops the background polling loop. /// public void Stop() { _cts?.Cancel(); try { _pollTask?.Wait(TimeSpan.FromSeconds(5)); } catch { /* timeout or faulted */ } _pollTask = null; Log.Information("Change detection stopped"); } private async Task PollLoopAsync(CancellationToken ct) { // If no initial deploy time was provided, first poll triggers unconditionally var firstPoll = LastKnownDeployTime == null; while (!ct.IsCancellationRequested) { try { var deployTime = await _repository.GetLastDeployTimeAsync(ct); if (firstPoll) { firstPoll = false; LastKnownDeployTime = deployTime; Log.Information("Initial deploy time: {DeployTime}", deployTime); OnGalaxyChanged?.Invoke(); } else if (deployTime != LastKnownDeployTime) { Log.Information("Galaxy deployment change detected: {Previous} → {Current}", LastKnownDeployTime, deployTime); LastKnownDeployTime = deployTime; OnGalaxyChanged?.Invoke(); } } catch (OperationCanceledException) { break; } catch (Exception ex) { Log.Warning(ex, "Change detection poll failed, will retry next interval"); } try { await Task.Delay(TimeSpan.FromSeconds(_intervalSeconds), ct); } catch (OperationCanceledException) { break; } } } } }