Dreamine.Communication.Core 1.0.2
Dreamine.Communication.Core 통신 기능과 관련 API를 제공합니다.
로딩중...
검색중...
일치하는것 없음
ResilientMessageTransport.cs
이 파일의 문서화 페이지로 가기
1using Dreamine.Communication.Abstractions.Enums;
2using Dreamine.Communication.Abstractions.Interfaces;
3using Dreamine.Communication.Abstractions.Models;
4using Dreamine.Communication.Abstractions.Options;
6
8
17public sealed class ResilientMessageTransport : IMessageTransport
18{
27 private readonly IMessageTransport _innerTransport;
36 private readonly ReconnectPolicy _reconnectPolicy;
45 private readonly OutboundQueueOptions _queueOptions;
72 private readonly SemaphoreSlim _flushLock = new(1, 1);
81 private readonly object _loopSync = new();
90 private readonly object _notifySync = new();
91
100 private CancellationTokenSource _lifetimeCts = new();
109 private Task? _resilienceLoopTask;
136 private int _disposeGuard; // Interlocked: 0 = live, 1 = disposed
145 private bool _disposed;
154 private ConnectionState _lastNotifiedState = ConnectionState.Disconnected;
155
205 IMessageTransport innerTransport,
206 ReconnectPolicy? reconnectPolicy = null,
207 OutboundQueueOptions? queueOptions = null,
208 IOutboundMessageQueue? outboundQueue = null)
209 {
210 _innerTransport = innerTransport ?? throw new ArgumentNullException(nameof(innerTransport));
211 _reconnectPolicy = reconnectPolicy ?? new ReconnectPolicy();
212 _queueOptions = queueOptions ?? new OutboundQueueOptions();
215
217 _innerTransport.MessageReceived += OnInnerMessageReceived;
218 }
219
228 public event EventHandler<MessageEnvelope>? MessageReceived;
229
238 public event EventHandler<ConnectionState>? StateChanged;
239
248 public event EventHandler<int>? QueueCountChanged;
249
258 public TransportKind Kind => _innerTransport.Kind;
259
268 public ConnectionState State
269 {
270 get
271 {
273 {
274 return _innerTransport.State;
275 }
276
277 if (_reconnectPolicy.Enabled &&
280 {
281 return ConnectionState.Connecting;
282 }
283
284 return _innerTransport.State;
285 }
286 }
287
297
338 public async Task ConnectAsync(CancellationToken cancellationToken = default)
339 {
341
345
346 try
347 {
348 await _innerTransport.ConnectAsync(cancellationToken).ConfigureAwait(false);
350
351 if (_queueOptions.FlushOnReconnect)
352 {
353 await FlushQueueAsync(cancellationToken).ConfigureAwait(false);
354 }
355 }
356 catch
357 {
359
360 if (!_reconnectPolicy.Enabled)
361 {
362 throw;
363 }
364
365 // 자동 재접속 모드에서는 최초 연결 실패를 치명 오류로 보지 않습니다.
366 // 이후 백그라운드 재접속 루프가 계속 연결을 시도합니다.
367 }
368 }
369
410 public async Task DisconnectAsync(CancellationToken cancellationToken = default)
411 {
413
417
418 await _innerTransport.DisconnectAsync(cancellationToken).ConfigureAwait(false);
420 }
421
486 public async Task SendAsync(
487 MessageEnvelope message,
488 CancellationToken cancellationToken = default)
489 {
490 ArgumentNullException.ThrowIfNull(message);
492
494
496 {
497 try
498 {
499 await _innerTransport.SendAsync(message, cancellationToken).ConfigureAwait(false);
501 return;
502 }
503 catch
504 {
505 await HandleDisconnectedSendAsync(message, cancellationToken).ConfigureAwait(false);
508 return;
509 }
510 }
511
512 await HandleDisconnectedSendAsync(message, cancellationToken).ConfigureAwait(false);
515 }
516
557 public async Task FlushQueueAsync(CancellationToken cancellationToken = default)
558 {
560
561 if (!_queueOptions.FlushOnReconnect)
562 {
563 return;
564 }
565
566 await _flushLock.WaitAsync(cancellationToken).ConfigureAwait(false);
567
568 try
569 {
571 {
572 cancellationToken.ThrowIfCancellationRequested();
573
574 var queuedMessage = await _outboundQueue
575 .TryDequeueAsync(cancellationToken)
576 .ConfigureAwait(false);
577
578 if (queuedMessage is null)
579 {
582 return;
583 }
584
586
587 if (queuedMessage.IsExpired(
588 _queueOptions.MaxMessageAge,
589 DateTimeOffset.UtcNow))
590 {
591 continue;
592 }
593
594 try
595 {
596 await _innerTransport
597 .SendAsync(queuedMessage.Message, cancellationToken)
598 .ConfigureAwait(false);
599
602 }
603 catch (Exception ex)
604 {
605 await _outboundQueue
606 .EnqueueFrontAsync(queuedMessage.WithFailure(ex), cancellationToken)
607 .ConfigureAwait(false);
608
612 return;
613 }
614 }
615 }
616 finally
617 {
618 _flushLock.Release();
619 }
620 }
621
638 public async ValueTask DisposeAsync()
639 {
640 // Interlocked guard prevents concurrent double-dispose.
641 if (Interlocked.CompareExchange(ref _disposeGuard, 1, 0) != 0)
642 {
643 return;
644 }
645
646 _disposed = true;
649
650 _innerTransport.MessageReceived -= OnInnerMessageReceived;
651
652 try
653 {
654 await _lifetimeCts.CancelAsync().ConfigureAwait(false);
655 }
656 catch
657 {
658 // Dispose 경로에서는 취소 실패를 무시합니다.
659 }
660
661 if (_resilienceLoopTask is not null)
662 {
663 try
664 {
665 await _resilienceLoopTask.ConfigureAwait(false);
666 }
667 catch
668 {
669 // Dispose 경로에서는 백그라운드 루프 예외를 무시합니다.
670 }
671 }
672
673 _lifetimeCts.Dispose();
674 _flushLock.Dispose();
675
676 await _innerTransport.DisposeAsync().ConfigureAwait(false);
677 }
678
719 private async Task HandleDisconnectedSendAsync(
720 MessageEnvelope message,
721 CancellationToken cancellationToken)
722 {
723 switch (_queueOptions.DisconnectedSendPolicy)
724 {
725 case DisconnectedSendPolicy.Fail:
726 throw new InvalidOperationException("Transport is not connected.");
727
728 case DisconnectedSendPolicy.Queue:
729 await _outboundQueue
730 .EnqueueAsync(new QueuedOutboundMessage(message), cancellationToken)
731 .ConfigureAwait(false);
732
734 return;
735
736 case DisconnectedSendPolicy.WaitUntilConnected:
737 await WaitUntilConnectedAsync(cancellationToken).ConfigureAwait(false);
738 await _innerTransport.SendAsync(message, cancellationToken).ConfigureAwait(false);
740 return;
741
742 default:
743 throw new InvalidOperationException(
744 $"Unsupported disconnected send policy: {_queueOptions.DisconnectedSendPolicy}");
745 }
746 }
747
780 private async Task WaitUntilConnectedAsync(CancellationToken cancellationToken)
781 {
784
785 while (!IsOperationalState(_innerTransport.State))
786 {
787 cancellationToken.ThrowIfCancellationRequested();
788 await Task.Delay(_reconnectPolicy.WatchInterval, cancellationToken)
789 .ConfigureAwait(false);
790 }
791 }
792
802 {
803 if (!_reconnectPolicy.Enabled)
804 {
805 return;
806 }
807
808 lock (_loopSync)
809 {
810 if (_disposed)
811 {
812 return;
813 }
814
816
817 if (_resilienceLoopTask is { IsCompleted: false })
818 {
819 return;
820 }
821
822 if (_lifetimeCts.IsCancellationRequested)
823 {
824 _lifetimeCts.Dispose();
825 _lifetimeCts = new CancellationTokenSource();
826 }
827
828 _resilienceLoopTask = Task.Run(
830 CancellationToken.None);
831 }
832 }
833
858 private async Task RunResilienceLoopAsync(CancellationToken cancellationToken)
859 {
860 var reconnectAttempt = 0;
861
862 while (!cancellationToken.IsCancellationRequested)
863 {
865
867 {
868 await DelaySafelyAsync(
869 _reconnectPolicy.WatchInterval,
870 cancellationToken)
871 .ConfigureAwait(false);
872
873 continue;
874 }
875
877 {
878 reconnectAttempt = 0;
880
881 if (_queueOptions.FlushOnReconnect && _outboundQueue.Count > 0)
882 {
883 await FlushQueueAsync(cancellationToken).ConfigureAwait(false);
884 }
885
886 await DelaySafelyAsync(
887 _reconnectPolicy.WatchInterval,
888 cancellationToken)
889 .ConfigureAwait(false);
890
891 continue;
892 }
893
894 if (_innerTransport.State == ConnectionState.Disconnecting)
895 {
896 await DelaySafelyAsync(
897 _reconnectPolicy.WatchInterval,
898 cancellationToken)
899 .ConfigureAwait(false);
900
901 continue;
902 }
903
904 if (_reconnectPolicy.MaxRetryCount is not null &&
905 reconnectAttempt >= _reconnectPolicy.MaxRetryCount.Value)
906 {
907 await DelaySafelyAsync(
908 _reconnectPolicy.MaxDelay,
909 cancellationToken)
910 .ConfigureAwait(false);
911
912 continue;
913 }
914
915 reconnectAttempt++;
917
918 try
919 {
920 await _innerTransport.ConnectAsync(cancellationToken).ConfigureAwait(false);
921 reconnectAttempt = 0;
923
924 if (_queueOptions.FlushOnReconnect)
925 {
926 await FlushQueueAsync(cancellationToken).ConfigureAwait(false);
927 }
928 }
929 catch
930 {
932
933 var delay = _delayCalculator.Calculate(reconnectAttempt);
934
935 await DelaySafelyAsync(delay, cancellationToken)
936 .ConfigureAwait(false);
937 }
938 }
939 }
940
965 private static bool IsOperationalState(ConnectionState state)
966 {
967 return state is ConnectionState.Connected or ConnectionState.Listening;
968 }
969
979 Volatile.Read(ref _manualDisconnectRequested) != 0;
980
990 Volatile.Read(ref _resilienceLoopRequested) != 0;
991
1008 private void SetManualDisconnectRequested(bool value)
1009 {
1010 Interlocked.Exchange(ref _manualDisconnectRequested, value ? 1 : 0);
1011 }
1012
1029 private void SetResilienceLoopRequested(bool value)
1030 {
1031 Interlocked.Exchange(ref _resilienceLoopRequested, value ? 1 : 0);
1032 }
1033
1066 private static async Task DelaySafelyAsync(
1067 TimeSpan delay,
1068 CancellationToken cancellationToken)
1069 {
1070 if (delay <= TimeSpan.Zero)
1071 {
1072 delay = TimeSpan.FromMilliseconds(1);
1073 }
1074
1075 try
1076 {
1077 await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
1078 }
1079 catch (OperationCanceledException)
1080 {
1081 // 정상적인 종료 경로입니다.
1082 }
1083 }
1084
1109 private void OnInnerMessageReceived(object? sender, MessageEnvelope message)
1110 {
1111 MessageReceived?.Invoke(this, message);
1112 }
1113
1123 {
1124 var currentState = State;
1125 EventHandler<ConnectionState>? handler;
1126
1127 lock (_notifySync)
1128 {
1129 if (_lastNotifiedState == currentState)
1130 {
1131 return;
1132 }
1133
1134 _lastNotifiedState = currentState;
1135 handler = StateChanged;
1136 }
1137
1138 handler?.Invoke(this, currentState);
1139 }
1140
1150 {
1151 QueueCountChanged?.Invoke(this, _outboundQueue.Count);
1152 }
1153
1170 private void ThrowIfDisposed()
1171 {
1172 ObjectDisposedException.ThrowIf(_disposed, this);
1173 }
1174}
async Task ConnectAsync(CancellationToken cancellationToken=default)
async Task SendAsync(MessageEnvelope message, CancellationToken cancellationToken=default)
static async Task DelaySafelyAsync(TimeSpan delay, CancellationToken cancellationToken)
ResilientMessageTransport(IMessageTransport innerTransport, ReconnectPolicy? reconnectPolicy=null, OutboundQueueOptions? queueOptions=null, IOutboundMessageQueue? outboundQueue=null)
async Task WaitUntilConnectedAsync(CancellationToken cancellationToken)
async Task FlushQueueAsync(CancellationToken cancellationToken=default)
async Task RunResilienceLoopAsync(CancellationToken cancellationToken)
async Task DisconnectAsync(CancellationToken cancellationToken=default)
async Task HandleDisconnectedSendAsync(MessageEnvelope message, CancellationToken cancellationToken)