1818#endregion
1919
2020using System ;
21- using System . Collections . Concurrent ;
2221using System . Text ;
2322using System . Threading ;
24- using System . Threading . Tasks ;
2523using log4net . Appender . Internal ;
2624using log4net . Core ;
2725using log4net . Layout ;
@@ -279,9 +277,11 @@ public enum SyslogNewLineHandling
279277 Keep
280278 }
281279
282- private readonly BlockingCollection < byte [ ] > _sendQueue = new ( ) ;
283- private CancellationTokenSource ? _cancellationTokenSource ;
284- private Task ? _pumpTask ;
280+ private const int CloseTimeoutMillis = 5_000 ;
281+ private IUdpConnection ? _connection ;
282+ private BackgroundSender < byte [ ] > ? _sender ;
283+ private int _sendQueueSize = 500 ;
284+ private int _enqueueTimeoutMillis = 5_000 ;
285285
286286 /// <summary>
287287 /// Initializes a new instance of the <see cref="RemoteSyslogAppender" /> class.
@@ -389,7 +389,8 @@ protected override void Append(LoggingEvent loggingEvent)
389389 // Grab as a byte array
390390 byte [ ] buffer = Encoding . GetBytes ( builder . ToString ( ) ) ;
391391
392- _sendQueue . Add ( buffer ) ;
392+ // A full queue is reported by the sender itself.
393+ _sender ? . TryEnqueue ( buffer , EnqueueTimeoutMillis ) ;
393394 }
394395 }
395396 catch ( Exception e ) when ( ! e . IsFatal ( ) )
@@ -524,10 +525,89 @@ public override void ActivateOptions()
524525 $ "The NewLineHandling is not { SyslogNewLineHandling . Escape } or { SyslogNewLineHandling . Keep } or { SyslogNewLineHandling . Split } .") ;
525526 }
526527 _levelMapping . ActivateOptions ( ) ;
527- // Start the background pump
528- _cancellationTokenSource = new ( ) ;
529- _pumpTask = Task . Factory . StartNew ( ( ) => ProcessQueueAsync ( _cancellationTokenSource . Token ) , CancellationToken . None ,
530- TaskCreationOptions . LongRunning , TaskScheduler . Default ) ;
528+ StopSending ( ) ;
529+
530+ IUdpConnection connection = CreateUdpConnection ( ) ;
531+ try
532+ {
533+ connection . Connect ( LocalPort , RemoteAddress . EnsureNotNull ( ) , RemotePort ) ;
534+ }
535+ catch ( Exception e ) when ( ! e . IsFatal ( ) )
536+ {
537+ ErrorHandler . Error (
538+ $ "Unable to connect to remote syslog { RemoteAddress } on port { RemotePort } from local port { LocalPort } . "
539+ + "No logging event will be sent." ,
540+ e , ErrorCode . GenericFailure ) ;
541+ connection . Dispose ( ) ;
542+ return ;
543+ }
544+
545+ _connection = connection ;
546+ _sender = new ( nameof ( RemoteSyslogAppender ) , SendQueueSize , Send , Report ) ;
547+ }
548+
549+ /// <summary>
550+ /// How many datagrams may wait to be sent. Defaults to 500.
551+ /// </summary>
552+ /// <exception cref="ArgumentOutOfRangeException">The value specified is not positive.</exception>
553+ public int SendQueueSize
554+ {
555+ get => _sendQueueSize ;
556+ set
557+ {
558+ if ( value <= 0 )
559+ {
560+ throw SystemInfo . CreateArgumentOutOfRangeException ( nameof ( value ) , value ,
561+ "The value specified for SendQueueSize is not positive." ) ;
562+ }
563+ _sendQueueSize = value ;
564+ }
565+ }
566+
567+ /// <summary>
568+ /// How long a logging call waits for room in a full queue. Defaults to 5000, 0 never waits.
569+ /// </summary>
570+ /// <exception cref="ArgumentOutOfRangeException">The value specified is negative.</exception>
571+ public int EnqueueTimeoutMillis
572+ {
573+ get => _enqueueTimeoutMillis ;
574+ set
575+ {
576+ if ( value < 0 )
577+ {
578+ throw SystemInfo . CreateArgumentOutOfRangeException ( nameof ( value ) , value ,
579+ "The value specified for EnqueueTimeoutMillis is negative." ) ;
580+ }
581+ _enqueueTimeoutMillis = value ;
582+ }
583+ }
584+
585+ private void Send ( byte [ ] datagram , CancellationToken cancellationToken )
586+ => _connection ? . SendAsync ( datagram , datagram . Length ) . GetAwaiter ( ) . GetResult ( ) ;
587+
588+ private void Report ( string message , Exception ? exception )
589+ {
590+ if ( exception is null )
591+ {
592+ ErrorHandler . Error ( message ) ;
593+ }
594+ else
595+ {
596+ ErrorHandler . Error ( message , exception ) ;
597+ }
598+ }
599+
600+ private void StopSending ( )
601+ {
602+ if ( _sender is BackgroundSender < byte [ ] > sender )
603+ {
604+ _sender = null ;
605+ sender . Close ( CloseTimeoutMillis ) ;
606+ sender . Dispose ( ) ;
607+ }
608+
609+ _connection ? . Dispose ( ) ;
610+ _connection = null ;
531611 }
532612
533613 /// <summary>
@@ -636,12 +716,17 @@ public class LevelSeverity : LevelMappingEntry
636716 public SyslogSeverity Severity { get ; set ; }
637717 }
638718
719+ /// <summary>
720+ /// Waits until everything queued so far has been sent.
721+ /// </summary>
722+ /// <param name="millisecondsTimeout">The maximum time to wait.</param>
723+ /// <returns><see langword="false"/> on timeout, or when the sender is no longer running.</returns>
724+ public override bool Flush ( int millisecondsTimeout ) => _sender ? . Flush ( millisecondsTimeout ) ?? true ;
725+
639726 /// <inheritdoc/>
640727 protected override void OnClose ( )
641728 {
642- // Signal shutdown and wait for the pump to drain
643- _cancellationTokenSource ? . Cancel ( ) ;
644- _pumpTask ? . Wait ( TimeSpan . FromSeconds ( 5 ) ) ;
729+ StopSending ( ) ;
645730 base . OnClose ( ) ;
646731 }
647732
@@ -653,58 +738,4 @@ protected override void OnClose()
653738 protected override void InitializeClientConnection ( )
654739 {
655740 }
656-
657- private async Task ProcessQueueAsync ( CancellationToken token )
658- {
659- // We create our own UdpClient here, so that client lifetime is tied to this task
660- IUdpConnection udpClient ;
661- try
662- {
663- udpClient = CreateUdpConnection ( ) ;
664- udpClient . Connect ( LocalPort , RemoteAddress . EnsureNotNull ( ) , RemotePort ) ;
665- }
666- catch ( Exception e ) when ( ! e . IsFatal ( ) )
667- {
668- // Outside the loop below, so this would otherwise fault the pump unobserved and leave
669- // every later event queued behind a sender that is no longer running.
670- ErrorHandler . Error (
671- $ "Unable to connect to remote syslog { RemoteAddress } on port { RemotePort } from local port { LocalPort } . "
672- + "No logging event will be sent." ,
673- e , ErrorCode . GenericFailure ) ;
674- return ;
675- }
676-
677- using IUdpConnection connection = udpClient ;
678- try
679- {
680- while ( ! token . IsCancellationRequested )
681- {
682- // Take next message or throw when canceled
683- byte [ ] datagram = _sendQueue . Take ( token ) ;
684- try
685- {
686- await udpClient . SendAsync ( datagram , datagram . Length ) . ConfigureAwait ( false ) ;
687- }
688- catch ( Exception ex ) when ( ! ex . IsFatal ( ) )
689- {
690- ErrorHandler . Error ( "RemoteSyslogAppender: send failed" , ex , ErrorCode . WriteFailure ) ;
691- }
692- }
693- }
694- catch ( OperationCanceledException )
695- {
696- // Clean shutdown: drain remaining items if desired
697- while ( _sendQueue . TryTake ( out byte [ ] ? leftover ) )
698- {
699- try
700- {
701- await udpClient . SendAsync ( leftover , leftover . Length ) . ConfigureAwait ( false ) ;
702- }
703- catch ( Exception ex ) when ( ! ex . IsFatal ( ) )
704- {
705- ErrorHandler . Error ( "RemoteSyslogAppender: send failed during shutdown" , ex , ErrorCode . FlushFailure ) ;
706- }
707- }
708- }
709- }
710741}
0 commit comments