using Microsoft.Extensions.Logging; using ScadaLink.Commons.Types.Enums; namespace ScadaLink.StoreAndForward; /// /// WP-11: Async replication of buffer operations to standby node. /// /// - Forwards add/remove/park operations to standby via a replication handler. /// - No ack wait (fire-and-forget per design). /// - Standby applies operations to its own SQLite. /// - On failover, standby resumes delivery from its replicated state. /// public class ReplicationService { private readonly StoreAndForwardOptions _options; private readonly ILogger _logger; private Func? _replicationHandler; public ReplicationService( StoreAndForwardOptions options, ILogger logger) { _options = options; _logger = logger; } /// /// Sets the handler for forwarding replication operations to the standby node. /// Typically wraps Akka Tell to the standby's replication actor. /// public void SetReplicationHandler(Func handler) { _replicationHandler = handler; } /// /// WP-11: Replicates an enqueue operation to standby (fire-and-forget). /// public void ReplicateEnqueue(StoreAndForwardMessage message) { if (!_options.ReplicationEnabled || _replicationHandler == null) return; FireAndForget(new ReplicationOperation( ReplicationOperationType.Add, message.Id, message)); } /// /// WP-11: Replicates a remove operation to standby (fire-and-forget). /// public void ReplicateRemove(string messageId) { if (!_options.ReplicationEnabled || _replicationHandler == null) return; FireAndForget(new ReplicationOperation( ReplicationOperationType.Remove, messageId, null)); } /// /// WP-11: Replicates a park operation to standby (fire-and-forget). /// public void ReplicatePark(StoreAndForwardMessage message) { if (!_options.ReplicationEnabled || _replicationHandler == null) return; FireAndForget(new ReplicationOperation( ReplicationOperationType.Park, message.Id, message)); } /// /// WP-11: Applies a replicated operation received from the active node. /// Used by the standby node to keep its SQLite in sync. /// public async Task ApplyReplicatedOperationAsync( ReplicationOperation operation, StoreAndForwardStorage storage) { switch (operation.OperationType) { case ReplicationOperationType.Add when operation.Message != null: await storage.EnqueueAsync(operation.Message); break; case ReplicationOperationType.Remove: await storage.RemoveMessageAsync(operation.MessageId); break; case ReplicationOperationType.Park when operation.Message != null: operation.Message.Status = StoreAndForwardMessageStatus.Parked; await storage.UpdateMessageAsync(operation.Message); break; } } private void FireAndForget(ReplicationOperation operation) { Task.Run(async () => { try { await _replicationHandler!.Invoke(operation); } catch (Exception ex) { // WP-11: No ack wait — log and move on _logger.LogDebug(ex, "Replication of {OpType} for message {MessageId} failed (best-effort)", operation.OperationType, operation.MessageId); } }); } } /// /// WP-11: Represents a buffer operation to be replicated to standby. /// public record ReplicationOperation( ReplicationOperationType OperationType, string MessageId, StoreAndForwardMessage? Message); /// /// WP-11: Types of buffer operations that are replicated. /// public enum ReplicationOperationType { Add, Remove, Park }