This is an automated email from the ASF dual-hosted git repository.

freeandnil pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git


The following commit(s) were added to refs/heads/master by this push:
     new 592d18de Reduce silent log event loss during XmlConfigurator 
reconfiguration (#287)
592d18de is described below

commit 592d18de651ebd330416ea68851c145660b68dc0
Author: Abdullah hamza <[email protected]>
AuthorDate: Fri Apr 10 10:35:51 2026 +0300

    Reduce silent log event loss during XmlConfigurator reconfiguration (#287)
    
    * feat(Logger): add ReplaceAppenders for atomic appender swap
    
    Add method to atomically replace all appenders with a new collection.
    
    * Enable diagnostics by resetting Emfix(Hierarchy): reset 
EmittedNoAppenderWarning in XmlConfigurator
    
    Reset the warning flag to allow diagnostics during reconfiguration.
    
    * fix(XmlHierarchyConfigurator): collect appenders before swap to minimize 
null window
    
    Previously, ParseChildrenOfLoggerElement called RemoveAllAppenders()
    before adding new ones one-by-one, creating a window (equal to the full
    XML parse duration, typically 10-50 ms) during which the logger had no
    appenders and log events were silently dropped.
    
    This change resolves the race by:
    1. Collecting all incoming appenders into a local List<IAppender> first.
    2. Calling Logger.ReplaceAppenders() to perform the swap in a single
       writer lock, reducing the zero-appender window to microseconds.
    
    * Fix: resolve flaky SmtpPickupDirAppenderTest.TestOutputContainsSentDate 
in CI
---
 .../Appender/SmtpPickupDirAppenderTest.cs          |  7 +--
 src/log4net.Tests/Hierarchy/LoggerTest.cs          | 62 ++++++++++++++++++++++
 .../Hierarchy/XmlHierarchyConfiguratorTest.cs      |  1 +
 src/log4net/Repository/Hierarchy/Hierarchy.cs      |  2 +-
 src/log4net/Repository/Hierarchy/Logger.cs         | 35 +++++++++++-
 .../Hierarchy/XmlHierarchyConfigurator.cs          | 62 ++++++++++++++--------
 6 files changed, 142 insertions(+), 27 deletions(-)

diff --git a/src/log4net.Tests/Appender/SmtpPickupDirAppenderTest.cs 
b/src/log4net.Tests/Appender/SmtpPickupDirAppenderTest.cs
index da1eb3b9..2817f6dc 100644
--- a/src/log4net.Tests/Appender/SmtpPickupDirAppenderTest.cs
+++ b/src/log4net.Tests/Appender/SmtpPickupDirAppenderTest.cs
@@ -163,6 +163,7 @@ public void TestOutputContainsSentDate()
     SilentErrorHandler sh = new();
     SmtpPickupDirAppender appender = CreateSmtpPickupDirAppender(sh);
     ILogger log = CreateLogger(appender);
+    DateTime beforeLog = DateTime.UtcNow;
     log.Log(GetType(), Level.Info, "This is a message", null);
     log.Log(GetType(), Level.Info, "This is a message 2", null);
     DestroyLogger();
@@ -177,9 +178,9 @@ public void TestOutputContainsSentDate()
       {
         string datePart = line.Substring(dateHeaderStart.Length);
         DateTime date = DateTime.ParseExact(datePart, "r", 
System.Globalization.CultureInfo.InvariantCulture);
-        double diff = Math.Abs((DateTime.UtcNow - date).TotalMilliseconds);
-        Assert.That(diff, Is.LessThanOrEqualTo(1000),
-          "Times should be equal, allowing a diff of one second to make test 
robust");
+        double diff = Math.Abs((date - beforeLog).TotalMilliseconds);
+        Assert.That(diff, Is.LessThanOrEqualTo(5000),
+          "Times should be equal, allowing a diff of five seconds to make test 
robust");
         hasDateHeader = true;
       }
     }
diff --git a/src/log4net.Tests/Hierarchy/LoggerTest.cs 
b/src/log4net.Tests/Hierarchy/LoggerTest.cs
index dd10b463..40728894 100644
--- a/src/log4net.Tests/Hierarchy/LoggerTest.cs
+++ b/src/log4net.Tests/Hierarchy/LoggerTest.cs
@@ -19,6 +19,7 @@
 
 using System;
 using System.Collections;
+using System.Threading;
 using log4net.Appender;
 using log4net.Core;
 using log4net.Repository.Hierarchy;
@@ -342,4 +343,65 @@ public void TestHierarchy1()
     Logger a1 = (Logger)h.GetLogger("a");
     Assert.That(a1, Is.SameAs(a0));
   }
+
+  /// <summary>
+  /// Tests the ReplaceAppenders method to ensure it replaces existing 
appenders
+  /// </summary>
+  [Test]
+  public void TestReplaceAppenders()
+  {
+    Logger log = (Logger)LogManager.GetLogger("ReplaceAppendersTest").Logger;
+    CountingAppender a1 = new() { Name = "testAppender1" };
+    log.AddAppender(a1);
+    Assert.That(log.Appenders, Has.Count.EqualTo(1));
+
+    CountingAppender a2 = new() { Name = "testAppender2" };
+    log.ReplaceAppenders(new IAppender[] { a2 });
+    Assert.That(log.Appenders, Has.Count.EqualTo(1));
+    Assert.That(log.GetAppender("testAppender1"), Is.Null);
+    Assert.That(log.GetAppender("testAppender2"), Is.SameAs(a2));
+  }
+
+  /// <summary>
+  /// Tests that ReplaceAppenders never leaves the logger in an appender-free 
state
+  /// that could cause silent log event loss during reconfiguration.
+  /// A reader thread continuously checks that the appender count never drops 
to zero
+  /// while the writer thread calls ReplaceAppenders.
+  /// </summary>
+  [Test]
+  public void TestReplaceAppendersIsAtomic()
+  {
+    Logger log = 
(Logger)LogManager.GetLogger("ReplaceAppendersAtomicTest").Logger;
+    CountingAppender initial = new() { Name = "initial" };
+    log.AddAppender(initial);
+
+    bool sawZeroAppenders = false;
+    bool stop = false;
+
+    // Reader thread: continuously observe the appender count
+    Thread reader = new(() =>
+    {
+      while (!stop)
+      {
+        if (log.Appenders.Count == 0)
+        {
+          sawZeroAppenders = true;
+        }
+      }
+    });
+    reader.Start();
+
+    // Writer thread: perform many rapid replacements
+    for (int i = 0; i < 1000; i++)
+    {
+      CountingAppender next = new() { Name = $"appender{i}" };
+      log.ReplaceAppenders(new IAppender[] { next });
+    }
+
+    stop = true;
+    reader.Join();
+
+    Assert.That(sawZeroAppenders, Is.False,
+      "ReplaceAppenders must never leave the logger with zero appenders 
(atomicity violation)");
+  }
 }
diff --git a/src/log4net.Tests/Hierarchy/XmlHierarchyConfiguratorTest.cs 
b/src/log4net.Tests/Hierarchy/XmlHierarchyConfiguratorTest.cs
index 13b31f29..6fdb5d7d 100644
--- a/src/log4net.Tests/Hierarchy/XmlHierarchyConfiguratorTest.cs
+++ b/src/log4net.Tests/Hierarchy/XmlHierarchyConfiguratorTest.cs
@@ -23,6 +23,7 @@
 using NUnit.Framework;
 
 using log4net.Repository.Hierarchy;
+using HierarchyClass = log4net.Repository.Hierarchy.Hierarchy;
 
 namespace log4net.Tests.Hierarchy;
 
diff --git a/src/log4net/Repository/Hierarchy/Hierarchy.cs 
b/src/log4net/Repository/Hierarchy/Hierarchy.cs
index 24be509d..452b5d7f 100644
--- a/src/log4net/Repository/Hierarchy/Hierarchy.cs
+++ b/src/log4net/Repository/Hierarchy/Hierarchy.cs
@@ -775,4 +775,4 @@ public override string ToString()
   /// </remarks>
   internal void AddProperty(PropertyEntry propertyEntry)
     => Properties[propertyEntry.Key.EnsureNotNull()] = 
propertyEntry.EnsureNotNull().Value;
-}
\ No newline at end of file
+}
diff --git a/src/log4net/Repository/Hierarchy/Logger.cs 
b/src/log4net/Repository/Hierarchy/Logger.cs
index 3fd13cc6..db52b68a 100644
--- a/src/log4net/Repository/Hierarchy/Logger.cs
+++ b/src/log4net/Repository/Hierarchy/Logger.cs
@@ -18,6 +18,7 @@
 #endregion
 
 using System;
+using System.Collections.Generic;
 
 using log4net.Appender;
 using log4net.Util;
@@ -594,4 +595,36 @@ protected virtual void ForcedLog(LoggingEvent logEvent)
 
     CallAppenders(logEvent);
   }
-}
\ No newline at end of file
+
+  /// <summary>
+  /// Atomically replaces all appenders with the provided collection.
+  /// </summary>
+  /// <param name="appenders">The new set of appenders to attach.</param>
+  /// <remarks>
+  /// <para>
+  /// This method removes the existing appenders and attaches all new
+  /// appenders inside a single writer lock, minimizing the window
+  /// during which the logger has no appenders. This reduces silent log
+  /// event loss during reconfiguration.
+  /// </para>
+  /// </remarks>
+  public virtual void ReplaceAppenders(IEnumerable<IAppender> appenders)
+  {
+    _appenderLock.AcquireWriterLock();
+    try
+    {
+      _appenderAttachedImpl?.RemoveAllAppenders();
+      _appenderAttachedImpl = null;
+
+      foreach (IAppender appender in appenders.EnsureNotNull())
+      {
+        _appenderAttachedImpl ??= new();
+        _appenderAttachedImpl.AddAppender(appender);
+      }
+    }
+    finally
+    {
+      _appenderLock.ReleaseWriterLock();
+    }
+  }
+}
diff --git a/src/log4net/Repository/Hierarchy/XmlHierarchyConfigurator.cs 
b/src/log4net/Repository/Hierarchy/XmlHierarchyConfigurator.cs
index 0e2f45eb..c65b719b 100644
--- a/src/log4net/Repository/Hierarchy/XmlHierarchyConfigurator.cs
+++ b/src/log4net/Repository/Hierarchy/XmlHierarchyConfigurator.cs
@@ -146,6 +146,9 @@ public void Configure(XmlElement? element)
       LogLog.Debug(_declaringType, "Configuration reset before reading 
config.");
     }
 
+    // A configuration is about to happen, so we can emit the warning again
+    hierarchy.EmittedNoAppenderWarning = false;
+
     /* Building Appender objects, placing them in a local namespace
        for future reference */
 
@@ -389,43 +392,58 @@ protected void ParseRoot(XmlElement rootElement)
   /// <para>
   /// Parse the child elements of a &lt;logger&gt; element.
   /// </para>
+  /// <para>
+  /// Appenders are collected from XML first, then applied atomically via
+  /// <see cref="Logger.ReplaceAppenders"/>, minimizing the window during
+  /// which the logger has no appenders and silent log event loss can occur.
+  /// </para>
   /// </remarks>
   protected void ParseChildrenOfLoggerElement(XmlElement catElement, Logger 
log, bool isRoot)
   {
-    // Remove all existing appenders from log. They will be
-    // reconstructed if need be.
-    log.EnsureNotNull().RemoveAllAppenders();
+    log.EnsureNotNull();
+    catElement.EnsureNotNull();
 
-    foreach (XmlNode currentNode in catElement.EnsureNotNull().ChildNodes)
+    // Phase 1: resolve all new appenders from XML *before* touching the
+    // live logger. This avoids the window where the logger has no appenders.
+    List<IAppender> newAppenders = new();
+
+    foreach (XmlNode currentNode in catElement.ChildNodes)
     {
-      if (currentNode.NodeType == XmlNodeType.Element)
+      if (currentNode.NodeType != XmlNodeType.Element)
       {
-        XmlElement currentElement = (XmlElement)currentNode;
+        continue;
+      }
 
-        if (currentElement.LocalName == AppenderRefTag)
-        {
-          string refName = currentElement.GetAttribute(RefAttr);
-          if (FindAppenderByReference(currentElement) is IAppender appender)
-          {
-            LogLog.Debug(_declaringType, $"Adding appender named [{refName}] 
to logger [{log.Name}].");
-            log.AddAppender(appender);
-          }
-          else
-          {
-            LogLog.Error(_declaringType, $"Appender named [{refName}] not 
found.");
-          }
-        }
-        else if (currentElement.LocalName is LevelTag or PriorityTag)
+      XmlElement currentElement = (XmlElement)currentNode;
+
+      if (currentElement.LocalName == AppenderRefTag)
+      {
+        string refName = currentElement.GetAttribute(RefAttr);
+        if (FindAppenderByReference(currentElement) is IAppender appender)
         {
-          ParseLevel(currentElement, log, isRoot);
+          LogLog.Debug(_declaringType, $"Resolved appender [{refName}] for 
logger [{log.Name}].");
+          newAppenders.Add(appender);
         }
         else
         {
-          SetParameter(currentElement, log);
+          LogLog.Error(_declaringType, $"Appender named [{refName}] not 
found.");
         }
       }
+      else if (currentElement.LocalName is LevelTag or PriorityTag)
+      {
+        ParseLevel(currentElement, log, isRoot);
+      }
+      else
+      {
+        SetParameter(currentElement, log);
+      }
     }
 
+    // Phase 2: atomic swap — replace all appenders in one writer lock so
+    // the logger is never in a zero-appender state for longer than it takes
+    // to acquire and release the lock (microseconds, not milliseconds).
+    log.ReplaceAppenders(newAppenders);
+
     if (log is IOptionHandler optionHandler)
     {
       optionHandler.ActivateOptions();

Reply via email to