using Akka.Actor; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ScadaLink.Commons.Messages.Artifacts; using ScadaLink.Commons.Messages.DebugView; using ScadaLink.Commons.Messages.Deployment; using ScadaLink.Commons.Messages.Health; using ScadaLink.Commons.Messages.Integration; using ScadaLink.Commons.Messages.Lifecycle; using ScadaLink.Commons.Messages.RemoteQuery; namespace ScadaLink.Communication; /// /// Central-side service that wraps the Akka Ask pattern with per-pattern timeouts. /// Provides a typed API for sending messages to sites and awaiting responses. /// On connection drop, the ask times out (no central buffering per design). /// public class CommunicationService { private readonly CommunicationOptions _options; private readonly ILogger _logger; private IActorRef? _centralCommunicationActor; public CommunicationService( IOptions options, ILogger logger) { _options = options.Value; _logger = logger; } /// /// Sets the central communication actor reference. Called during actor system startup. /// public void SetCommunicationActor(IActorRef centralCommunicationActor) { _centralCommunicationActor = centralCommunicationActor; } private IActorRef GetActor() { return _centralCommunicationActor ?? throw new InvalidOperationException("CommunicationService not initialized. CentralCommunicationActor not set."); } // ── Pattern 1: Instance Deployment ── public async Task DeployInstanceAsync( string siteId, DeployInstanceCommand command, CancellationToken cancellationToken = default) { _logger.LogDebug( "Sending DeployInstanceCommand to site {SiteId}, instance={Instance}, correlationId={DeploymentId}", siteId, command.InstanceUniqueName, command.DeploymentId); var envelope = new SiteEnvelope(siteId, command); return await GetActor().Ask( envelope, _options.DeploymentTimeout, cancellationToken); } // ── Pattern 2: Lifecycle ── public async Task DisableInstanceAsync( string siteId, DisableInstanceCommand command, CancellationToken cancellationToken = default) { var envelope = new SiteEnvelope(siteId, command); return await GetActor().Ask( envelope, _options.LifecycleTimeout, cancellationToken); } public async Task EnableInstanceAsync( string siteId, EnableInstanceCommand command, CancellationToken cancellationToken = default) { var envelope = new SiteEnvelope(siteId, command); return await GetActor().Ask( envelope, _options.LifecycleTimeout, cancellationToken); } public async Task DeleteInstanceAsync( string siteId, DeleteInstanceCommand command, CancellationToken cancellationToken = default) { var envelope = new SiteEnvelope(siteId, command); return await GetActor().Ask( envelope, _options.LifecycleTimeout, cancellationToken); } // ── Pattern 3: Artifact Deployment ── public async Task DeployArtifactsAsync( string siteId, DeployArtifactsCommand command, CancellationToken cancellationToken = default) { var envelope = new SiteEnvelope(siteId, command); return await GetActor().Ask( envelope, _options.ArtifactDeploymentTimeout, cancellationToken); } // ── Pattern 4: Integration Routing ── public async Task RouteIntegrationCallAsync( string siteId, IntegrationCallRequest request, CancellationToken cancellationToken = default) { var envelope = new SiteEnvelope(siteId, request); return await GetActor().Ask( envelope, _options.IntegrationTimeout, cancellationToken); } // ── Pattern 5: Debug View ── public async Task SubscribeDebugViewAsync( string siteId, SubscribeDebugViewRequest request, CancellationToken cancellationToken = default) { var envelope = new SiteEnvelope(siteId, request); return await GetActor().Ask( envelope, _options.DebugViewTimeout, cancellationToken); } public void UnsubscribeDebugView(string siteId, UnsubscribeDebugViewRequest request) { // Tell (fire-and-forget) — no response expected GetActor().Tell(new SiteEnvelope(siteId, request)); } // ── Pattern 6: Health Reporting (site→central, Tell) ── // Health reports are received by central, not sent. No method needed here. // ── Pattern 7: Remote Queries ── public async Task QueryEventLogsAsync( string siteId, EventLogQueryRequest request, CancellationToken cancellationToken = default) { var envelope = new SiteEnvelope(siteId, request); return await GetActor().Ask( envelope, _options.QueryTimeout, cancellationToken); } public async Task QueryParkedMessagesAsync( string siteId, ParkedMessageQueryRequest request, CancellationToken cancellationToken = default) { var envelope = new SiteEnvelope(siteId, request); return await GetActor().Ask( envelope, _options.QueryTimeout, cancellationToken); } // ── Pattern 8: Heartbeat (site→central, Tell) ── // Heartbeats are received by central, not sent. No method needed here. } /// /// Envelope that wraps any message with a target site ID for routing. /// Used by CentralCommunicationActor to resolve the site actor path. /// public record SiteEnvelope(string SiteId, object Message);