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

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


The following commit(s) were added to refs/heads/master by this push:
     new f5f0c240 Prevent potential data race when restarting the AsyncAppender 
dispatcher (#752)
f5f0c240 is described below

commit f5f0c24019b7274daad2508cab3de98d00359f1b
Author: Stephen Webb <[email protected]>
AuthorDate: Thu Sep 3 11:16:38 2026 +1000

    Prevent potential data race when restarting the AsyncAppender dispatcher 
(#752)
    
    * Limit use of 'bufferSize' property to initialization
    
    * Keep dispatcher thread alive even when an attached appender repeatedly 
throws an exception
---
 src/main/cpp/asyncappender.cpp         | 101 ++++++++++++++++++++-------------
 src/test/cpp/asyncappendertestcase.cpp |  75 ++++++++++++++++++++++++
 2 files changed, 138 insertions(+), 38 deletions(-)

diff --git a/src/main/cpp/asyncappender.cpp b/src/main/cpp/asyncappender.cpp
index ef12808d..d2ec3d36 100644
--- a/src/main/cpp/asyncappender.cpp
+++ b/src/main/cpp/asyncappender.cpp
@@ -139,10 +139,7 @@ struct AsyncAppender::AsyncAppenderPriv : public 
AppenderSkeleton::AppenderSkele
        using BaseType = AppenderSkeleton::AppenderSkeletonPrivate;
        AsyncAppenderPriv()
                : AppenderSkeletonPrivate()
-               , buffer(DEFAULT_BUFFER_SIZE)
                , bufferSize(DEFAULT_BUFFER_SIZE)
-               , dispatcher()
-               , locationInfo(false)
                , blocking(true)
 #if LOG4CXX_EVENTS_AT_EXIT
                , atExitRegistryRaii([this]{if (setClosed()) stopDispatcher();})
@@ -196,10 +193,23 @@ struct AsyncAppender::AsyncAppenderPriv : public 
AppenderSkeleton::AppenderSkele
         */
        std::thread dispatcher;
 
+       /**
+        * Serializes join()/joinable()/move-assignment on \c dispatcher:
+        * concurrent use of those operations on the same std::thread object
+        * is a data race with undefined behaviour.
+        */
+       std::mutex dispatcherMutex;
+
+       /**
+        * The dispatcher's thread id (written while holding \c dispatcherMutex,
+        * readable by logging threads without touching the thread object).
+        */
+       std::atomic<std::thread::id> dispatcherId{ std::thread::id() };
+
        /**
         * Used to determine when to restart dispatch thread.
        */
-       bool dispatcherActive{ false };
+       std::atomic<bool> dispatcherActive{ false };
 
        /**
         * Used to determine whether to restart dispatch thread.
@@ -216,24 +226,33 @@ struct AsyncAppender::AsyncAppenderPriv : public 
AppenderSkeleton::AppenderSkele
         */
        void checkDispatcher(const LogString& appenderName)
        {
+               if (this->dispatcherActive) // Fast path: no lock while the 
dispatcher is running
+                       return;
+
+               // A stopped (or not yet started) dispatcher may be observed by 
several
+               // logging threads concurrently; serialize all 
join()/joinable()/
+               // move-assignment on the thread object.
+               std::lock_guard<std::mutex> lock(this->dispatcherMutex);
+
                // Restart dispatcher if it has stopped by an exception in an 
attached appender.
                if (!this->dispatcherActive && this->dispatcher.joinable())
+               {
                        this->dispatcher.join();
+                       this->dispatcherId = std::thread::id();
+               }
 
                if (!this->dispatcher.joinable() && this->dispatcherStartCount 
<= 1)
                {
-                       std::lock_guard<std::recursive_mutex> lock(this->mutex);
-                       if (!this->dispatcher.joinable())
-                       {
-                               ++this->dispatcherStartCount;
-                               this->dispatcherActive = true;
-                               this->dispatcher = 
ThreadUtility::instance()->createThread
-                                       ( LOG4CXX_STR("AsyncAppender")
-                                       , 
&AsyncAppender::AsyncAppenderPriv::dispatch
-                                       , this
-                                       , appenderName
-                                       );
-                       }
+                       this->buffer.resize(this->bufferSize);
+                       ++this->dispatcherStartCount;
+                       this->dispatcherActive = true;
+                       this->dispatcher = 
ThreadUtility::instance()->createThread
+                               ( LOG4CXX_STR("AsyncAppender")
+                               , &AsyncAppender::AsyncAppenderPriv::dispatch
+                               , this
+                               , appenderName
+                               );
+                       this->dispatcherId = this->dispatcher.get_id();
                }
        }
 
@@ -242,9 +261,18 @@ struct AsyncAppender::AsyncAppenderPriv : public 
AppenderSkeleton::AppenderSkele
                bufferNotEmpty.notify_all();
                bufferNotFull.notify_all();
 
-               if (dispatcher.joinable())
+               // Move the thread object out under the lock but join outside 
it:
+               // the exiting dispatcher may still need bufferMutex, and a 
thread
+               // blocked on dispatcherMutex may be holding bufferMutex.
+               std::thread stoppedDispatcher;
+               {
+                       std::lock_guard<std::mutex> lock(dispatcherMutex);
+                       stoppedDispatcher = std::move(dispatcher);
+                       dispatcherId = std::thread::id();
+               }
+               if (stoppedDispatcher.joinable())
                {
-                       dispatcher.join();
+                       stoppedDispatcher.join();
                }
        }
 
@@ -253,7 +281,7 @@ struct AsyncAppender::AsyncAppenderPriv : public 
AppenderSkeleton::AppenderSkele
        /**
         * Should location info be included in dispatched messages.
        */
-       bool locationInfo;
+       bool locationInfo{ true };
 
        /**
         * Does appender block when buffer is full.
@@ -359,7 +387,7 @@ void AsyncAppender::append( 
LOG4CXX_APPEND_FORMAL_PARAMETERS )
 
        priv->checkDispatcher(getName());
 
-       if (priv->dispatcher.get_id() == std::this_thread::get_id()) // From an 
appender attached to this?
+       if (priv->dispatcherId.load() == std::this_thread::get_id()) // From an 
appender attached to this?
        {
                std::unique_lock<std::mutex> lock(priv->bufferMutex);
                auto loggerName = event->getLoggerName();
@@ -372,13 +400,13 @@ void AsyncAppender::append( 
LOG4CXX_APPEND_FORMAL_PARAMETERS )
        else while (true)
        {
                auto pendingCount = priv->eventCount - priv->dispatchedCount;
-               if (0 <= pendingCount && pendingCount < priv->bufferSize)
+               if (0 <= pendingCount && pendingCount < priv->buffer.size())
                {
                        // Claim a slot in the ring buffer
                        auto oldEventCount = priv->eventCount++;
                        auto index = oldEventCount % priv->buffer.size();
                        // Wait for a free slot
-                       while (priv->bufferSize <= oldEventCount - 
priv->dispatchedCount)
+                       while (priv->buffer.size() <= oldEventCount - 
priv->dispatchedCount)
                                std::this_thread::yield(); // Allow the 
dispatch thread to free a slot
                        // Write to the ring buffer
                        priv->buffer[index] = 
AsyncAppenderPriv::EventData{event, pendingCount};
@@ -412,7 +440,7 @@ void AsyncAppender::append( 
LOG4CXX_APPEND_FORMAL_PARAMETERS )
                        priv->bufferNotFull.wait(lock, [this]()
                        {
                                priv->checkDispatcher(getName());
-                               return priv->eventCount - priv->dispatchedCount 
< priv->bufferSize;
+                               return priv->eventCount - priv->dispatchedCount 
< priv->buffer.size();
                        });
                        --priv->blockedCount;
                        discard = false;
@@ -511,7 +539,6 @@ void AsyncAppender::setLocationInfo(bool flag)
        priv->locationInfo = flag;
 }
 
-
 void AsyncAppender::setBufferSize(int size)
 {
        if (size < 0)
@@ -519,20 +546,14 @@ void AsyncAppender::setBufferSize(int size)
                throw IllegalArgumentException(LOG4CXX_STR("size argument must 
be non-negative"));
        }
 
-       std::lock_guard<std::mutex> lock(priv->bufferMutex);
-       if (priv->dispatcher.joinable())
-       {
-               throw RuntimeException(LOG4CXX_STR("AsyncAppender buffer size 
cannot be changed now"));
-       }
+       std::lock_guard<std::mutex> lock(priv->dispatcherMutex);
        priv->bufferSize = (size < 1) ? 1 : size;
-       priv->buffer.resize(priv->bufferSize);
-       priv->bufferNotFull.notify_all();
 }
 
 int AsyncAppender::getBufferSize() const
 {
-       std::lock_guard<std::mutex> lock(priv->bufferMutex);
-       return priv->bufferSize;
+       std::lock_guard<std::mutex> lock(priv->dispatcherMutex);
+       return priv->buffer.empty() ? priv->bufferSize : 
static_cast<int>(priv->buffer.size());
 }
 
 void AsyncAppender::setBlocking(bool value)
@@ -628,13 +649,13 @@ void AsyncAppender::AsyncAppenderPriv::dispatch(const 
LogString& appenderName)
        size_t waitCount = 0;
        size_t producerBlockedCount = 0;
        int failureCount = 0;
-       std::vector<size_t> pendingCountHistogram(this->bufferSize, 0);
+       std::vector<size_t> pendingCountHistogram(this->buffer.size(), 0);
        bool isActive = true;
 
        while (isActive)
        {
                LoggingEventList events;
-               events.reserve(this->bufferSize);
+               events.reserve(this->buffer.size());
                for (int count = 0; count < 2 && this->dispatchedCount == 
this->commitCount; ++count)
                        std::this_thread::yield(); // Wait a bit
                if (this->dispatchedCount == this->commitCount)
@@ -647,7 +668,7 @@ void AsyncAppender::AsyncAppenderPriv::dispatch(const 
LogString& appenderName)
                }
                isActive = !this->isClosed();
 
-               while (events.size() < this->bufferSize && 
this->dispatchedCount != this->commitCount)
+               while (events.size() < this->buffer.size() && 
this->dispatchedCount != this->commitCount)
                {
                        auto index = this->dispatchedCount % 
this->buffer.size();
                        const auto& data = this->buffer[index];
@@ -668,6 +689,12 @@ void AsyncAppender::AsyncAppenderPriv::dispatch(const 
LogString& appenderName)
                        this->discardMap.clear();
                }
 
+               // A fault in an attached appender must not permanently disable 
this
+               // dispatch thread: producers using the default Blocking=true 
would
+               // hang forever once the ring buffer fills. Reset the failure 
budget
+               // for each batch so a transient fault (e.g. a temporarily full 
disk)
+               // only limits retries within the current batch.
+               failureCount = 0;
                for (auto item : events)
                {
                        try
@@ -690,8 +717,6 @@ void AsyncAppender::AsyncAppenderPriv::dispatch(const 
LogString& appenderName)
                        }
                }
                ++iterationCount;
-               if (1 < failureCount)
-                       break;
        }
        if (LogLog::isDebugEnabled())
        {
diff --git a/src/test/cpp/asyncappendertestcase.cpp 
b/src/test/cpp/asyncappendertestcase.cpp
index 265eced6..a2db6133 100644
--- a/src/test/cpp/asyncappendertestcase.cpp
+++ b/src/test/cpp/asyncappendertestcase.cpp
@@ -38,6 +38,7 @@
 #include <log4cxx/file.h>
 #include <ostream>
 #include <thread>
+#include <atomic>
 #include <fstream>
 
 #if LOG4CXX_ASYNC_BUFFER_SUPPORTS_FMT
@@ -124,6 +125,36 @@ class BlockableVectorAppender : public VectorAppender
 };
 LOG4CXX_PTR_DEF(BlockableVectorAppender);
 
+/**
+ * Vector appender that throws on the first \c failureCount calls to append.
+ */
+class TransientlyFailingVectorAppender : public VectorAppender
+{
+       private:
+               std::atomic<int> failuresRemaining;
+       public:
+               TransientlyFailingVectorAppender(int failureCount)
+                       : failuresRemaining(failureCount)
+               {
+               }
+
+               void append( LOG4CXX_APPEND_FORMAL_PARAMETERS ) override
+               {
+                       if (0 < failuresRemaining--)
+                       {
+                               throw RuntimeException(LOG4CXX_STR("Intentional 
transient exception"));
+                       }
+                       VectorAppender::append( LOG4CXX_APPEND_PARAMETERS );
+               }
+
+               /** Goes negative once an append call has succeeded. */
+               int remainingFailures() const
+               {
+                       return failuresRemaining;
+               }
+};
+LOG4CXX_PTR_DEF(TransientlyFailingVectorAppender);
+
 /**
  * An appender that adds logging events
  */
@@ -162,6 +193,7 @@ class AsyncAppenderTestCase : public 
AppenderSkeletonTestCase
                LOGUNIT_TEST(testEventFlush);
                LOGUNIT_TEST(testMultiThread);
                LOGUNIT_TEST(testBadAppender);
+               LOGUNIT_TEST(testDispatcherRecoversFromAppenderExceptions);
                LOGUNIT_TEST(testBufferOverflowBehavior);
                LOGUNIT_TEST(testLoggingAppender);
 #if LOG4CXX_HAS_DOMCONFIGURATOR
@@ -426,6 +458,49 @@ class AsyncAppenderTestCase : public 
AppenderSkeletonTestCase
                        LOGUNIT_ASSERT(0 < v.size());
                }
 
+               /**
+                * Checks the dispatch thread survives exceptions thrown by an
+                * attached appender. Regression test: two exceptions at any two
+                * points in the dispatch thread's lifetime used to stop 
dispatching
+                * permanently, after which producers using the default 
Blocking=true
+                * hung forever once the buffer filled.
+                */
+               void testDispatcherRecoversFromAppenderExceptions()
+               {
+                       // Configure Log4cxx
+                       AsyncAppenderPtr async;
+                       auto r = LogManager::getLoggerRepository();
+                       r->ensureIsConfigured([r, &async]()
+                       {
+                               async = std::make_shared<AsyncAppender>();
+                               
async->setName(LOG4CXX_STR("async-testDispatcherRecovers"));
+                               async->activateOptions();
+                               r->getRootLogger()->addAppender(async);
+                               r->setConfigured(true);
+                       });
+                       LOGUNIT_ASSERT(async);
+                       // The old code tolerated only 4 exceptions over the 
appender lifetime
+                       // (2 per dispatch thread incarnation, one restart 
allowed)
+                       auto failingAppender = 
std::make_shared<TransientlyFailingVectorAppender>(4);
+                       
failingAppender->setName(LOG4CXX_STR("async-transientlyFailingVector"));
+                       async->addAppender(failingAppender);
+
+                       // Log messages until one has been delivered (bounded 
wait)
+                       auto root = r->getRootLogger();
+                       for (int i = 0; 0 <= 
failingAppender->remainingFailures() && i < 100; i++)
+                       {
+                               LOG4CXX_INFO_ASYNC(root, "message" << i);
+                               std::this_thread::sleep_for( 
std::chrono::milliseconds( 10 ) );
+                       }
+                       LOG4CXX_INFO_ASYNC(root, "final message");
+                       async->close();
+
+                       // Check dispatching recovered once the transient fault 
cleared
+                       auto& v = failingAppender->getVector();
+                       LOGUNIT_ASSERT(!v.empty());
+                       LOGUNIT_ASSERT(v.back()->getRenderedMessage() == 
LOG4CXX_STR("final message"));
+               }
+
                /**
                 * Tests behavior when the the async buffer overflows.
                 */

Reply via email to