| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one or more | |
| * contributor license agreements. See the NOTICE file distributed with | |
| * this work for additional information regarding copyright ownership. | |
| * The ASF licenses this file to You under the Apache License, Version 2.0 | |
| * (the "License"); you may not use this file except in compliance with | |
| * the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| #include <log4cxx/asyncappender.h> | |
| #include <log4cxx/helpers/loglog.h> | |
| #include <log4cxx/spi/loggingevent.h> | |
| #include <log4cxx/helpers/stringhelper.h> | |
| #include <log4cxx/helpers/optionconverter.h> | |
| #include <log4cxx/helpers/threadutility.h> | |
| #include <log4cxx/private/appenderskeleton_priv.h> | |
| #include <thread> | |
| #include <atomic> | |
| #include <condition_variable> | |
| #if LOG4CXX_EVENTS_AT_EXIT | |
| #include <log4cxx/private/atexitregistry.h> | |
| #endif | |
| using namespace LOG4CXX_NS; | |
| using namespace LOG4CXX_NS::helpers; | |
| using namespace LOG4CXX_NS::spi; | |
| namespace | |
| { | |
| /** | |
| * The default buffer size is set to 128 events. | |
| */ | |
| enum { DEFAULT_BUFFER_SIZE = 128 }; | |
| class DiscardSummary | |
| { | |
| private: | |
| /** | |
| * First event of the highest severity. | |
| */ | |
| LoggingEventPtr maxEvent; | |
| /** | |
| * Total count of messages discarded. | |
| */ | |
| int count; | |
| /** | |
| * Why created | |
| */ | |
| LogString reason; | |
| public: | |
| /** | |
| * Create new instance. | |
| * | |
| * @param event must not be null. | |
| */ | |
| DiscardSummary(const LoggingEventPtr& event, const LogString& reason); | |
| /** Move values from \c src into a new instance. | |
| */ | |
| DiscardSummary(DiscardSummary&& src); | |
| /** Copy constructor. */ | |
| DiscardSummary(const DiscardSummary&) = delete; | |
| /** Assignment operator. */ | |
| DiscardSummary& operator=(const DiscardSummary&) = delete; | |
| /** | |
| * Add discarded event to summary. | |
| * | |
| * @param event event, may not be null. | |
| */ | |
| void add(const LoggingEventPtr& event); | |
| /** | |
| * Create an event with a discard count and the message from \c maxEvent. | |
| * | |
| * @return the new event. | |
| */ | |
| LoggingEventPtr createEvent(); | |
| /** | |
| * The number of messages discarded. | |
| */ | |
| int getCount() const { return count; } | |
| }; | |
| typedef std::map<LogString, DiscardSummary> DiscardMap; | |
| } | |
| #ifdef __cpp_lib_hardware_interference_size | |
| using std::hardware_constructive_interference_size; | |
| using std::hardware_destructive_interference_size; | |
| #else | |
| // 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │ ... | |
| constexpr std::size_t hardware_constructive_interference_size = 64; | |
| constexpr std::size_t hardware_destructive_interference_size = 64; | |
| #endif | |
| struct AsyncAppender::AsyncAppenderPriv : public AppenderSkeleton::AppenderSkeletonPrivate | |
| { | |
| using BaseType = AppenderSkeleton::AppenderSkeletonPrivate; | |
| AsyncAppenderPriv() | |
| : AppenderSkeletonPrivate() | |
| , bufferSize(DEFAULT_BUFFER_SIZE) | |
| , blocking(true) | |
| #if LOG4CXX_EVENTS_AT_EXIT | |
| , atExitRegistryRaii([this]{if (setClosed()) stopDispatcher();}) | |
| #endif | |
| , eventCount(0) | |
| , dispatchedCount(0) | |
| , commitCount(0) | |
| { } | |
| ~AsyncAppenderPriv() | |
| { | |
| if (setClosed()) | |
| close(); | |
| } | |
| /** | |
| * Event buffer. | |
| */ | |
| struct EventData | |
| { | |
| LoggingEventPtr event; | |
| size_t pendingCount; | |
| }; | |
| std::vector<EventData> buffer; | |
| /** | |
| * Mutex used to guard access to buffer and discardMap. | |
| */ | |
| std::mutex bufferMutex; | |
| std::condition_variable bufferNotFull; | |
| std::condition_variable bufferNotEmpty; | |
| /** | |
| * Map of DiscardSummary objects keyed by logger name. | |
| */ | |
| DiscardMap discardMap; | |
| /** | |
| * The maximum number of undispatched events. | |
| */ | |
| int bufferSize; | |
| /** | |
| * Nested appenders. | |
| */ | |
| helpers::AppenderAttachableImpl appenders; | |
| /** | |
| * Dispatcher. | |
| */ | |
| 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. | |
| */ | |
| std::atomic<bool> dispatcherActive{ false }; | |
| /** | |
| * Used to determine whether to restart dispatch thread. | |
| */ | |
| int dispatcherStartCount{ 0 }; | |
| /** | |
| * The function the dispatcher executes. | |
| */ | |
| void dispatch(const LogString& appenderName); | |
| /** | |
| * Start dispatcher if not already running. | |
| */ | |
| 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) | |
| { | |
| 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(); | |
| } | |
| } | |
| void stopDispatcher() | |
| { | |
| bufferNotEmpty.notify_all(); | |
| bufferNotFull.notify_all(); | |
| // 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()) | |
| { | |
| stoppedDispatcher.join(); | |
| } | |
| } | |
| void close(); | |
| /** | |
| * Does appender block when buffer is full. | |
| */ | |
| bool blocking; | |
| #if LOG4CXX_EVENTS_AT_EXIT | |
| helpers::AtExitRegistry::Raii atExitRegistryRaii; | |
| #endif | |
| /** | |
| * Used to calculate the buffer position at which to store the next event. | |
| */ | |
| alignas(hardware_constructive_interference_size) std::atomic<size_t> eventCount; | |
| /** | |
| * Used to calculate the buffer position from which to extract the next event. | |
| */ | |
| alignas(hardware_constructive_interference_size) std::atomic<size_t> dispatchedCount; | |
| /** | |
| * Used to communicate to the dispatch thread when an event is committed in buffer. | |
| */ | |
| alignas(hardware_constructive_interference_size) std::atomic<size_t> commitCount; | |
| bool isClosed() | |
| { | |
| std::lock_guard<std::mutex> lock(this->bufferMutex); | |
| return this->closed; | |
| } | |
| bool setClosed() | |
| { | |
| std::lock_guard<std::mutex> lock(this->bufferMutex); | |
| return BaseType::setClosed(); | |
| } | |
| /** | |
| * Used to ensure the dispatch thread does not wait when a logging thread is waiting. | |
| */ | |
| alignas(hardware_constructive_interference_size) int blockedCount{0}; | |
| }; | |
| IMPLEMENT_LOG4CXX_OBJECT(AsyncAppender) | |
| #define priv static_cast<AsyncAppenderPriv*>(m_priv.get()) | |
| AsyncAppender::AsyncAppender() | |
| : AppenderSkeleton(std::make_unique<AsyncAppenderPriv>()) | |
| { | |
| } | |
| AsyncAppender::~AsyncAppender() | |
| { | |
| } | |
| void AsyncAppender::addAppender(const AppenderPtr& newAppender) | |
| { | |
| priv->appenders.addAppender(newAppender); | |
| } | |
| void AsyncAppender::setOption(const LogString& option, | |
| const LogString& value) | |
| { | |
| if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("BUFFERSIZE"), LOG4CXX_STR("buffersize"))) | |
| { | |
| setBufferSize(OptionConverter::toInt(value, DEFAULT_BUFFER_SIZE)); | |
| } | |
| else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("BLOCKING"), LOG4CXX_STR("blocking"))) | |
| { | |
| setBlocking(OptionConverter::toBoolean(value, true)); | |
| } | |
| else | |
| { | |
| AppenderSkeleton::setOption(option, value); | |
| } | |
| } | |
| void AsyncAppender::doAppend( const spi::LoggingEventPtr& event ) | |
| { | |
| doAppendImpl( event ); | |
| } | |
| void AsyncAppender::append( const spi::LoggingEventPtr& event ) | |
| { | |
| if (priv->bufferSize <= 0) | |
| { | |
| priv->appenders.appendLoopOnAppenders(event); | |
| return; | |
| } | |
| // Get a copy of this thread's diagnostic context | |
| event->LoadDC(); | |
| priv->checkDispatcher(getName()); | |
| 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(); | |
| auto iter = priv->discardMap.find(loggerName); | |
| if (priv->discardMap.end() == iter) | |
| priv->discardMap.emplace(loggerName, DiscardSummary{ event, LOG4CXX_STR("from an attached appender") }); | |
| else | |
| iter->second.add(event); | |
| } | |
| else while (true) | |
| { | |
| auto pendingCount = priv->eventCount - priv->dispatchedCount; | |
| 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->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}; | |
| // Notify the dispatch thread that an event has been added | |
| auto failureCount = 0; | |
| auto savedEventCount = oldEventCount; | |
| while (!priv->commitCount.compare_exchange_weak(oldEventCount, oldEventCount + 1, std::memory_order_release)) | |
| { | |
| oldEventCount = savedEventCount; | |
| if (2 < ++failureCount) // Did the scheduler suspend a thread between claiming a slot and advancing commitCount? | |
| std::this_thread::yield(); // Wait a bit | |
| } | |
| priv->bufferNotEmpty.notify_all(); | |
| break; | |
| } | |
| // | |
| // Following code is only reachable if buffer is full or eventCount has overflowed | |
| // | |
| std::unique_lock<std::mutex> lock(priv->bufferMutex); | |
| priv->bufferNotEmpty.notify_all(); | |
| // | |
| // if blocking and thread is not already interrupted | |
| // and not the dispatcher then | |
| // wait for a buffer notification | |
| bool discard = true; | |
| if (priv->blocking | |
| && !priv->closed) | |
| { | |
| ++priv->blockedCount; | |
| priv->bufferNotFull.wait(lock, [this]() | |
| { | |
| priv->checkDispatcher(getName()); | |
| return priv->eventCount - priv->dispatchedCount < priv->buffer.size(); | |
| }); | |
| --priv->blockedCount; | |
| discard = false; | |
| } | |
| // | |
| // if blocking is false or thread has been interrupted | |
| // add event to discard map. | |
| // | |
| if (discard) | |
| { | |
| LogString loggerName = event->getLoggerName(); | |
| DiscardMap::iterator iter = priv->discardMap.find(loggerName); | |
| if (iter == priv->discardMap.end()) | |
| { | |
| priv->discardMap.emplace(loggerName, DiscardSummary{ event, LOG4CXX_STR("due to a full event buffer") }); | |
| } | |
| else | |
| { | |
| iter->second.add(event); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| void AsyncAppender::close() | |
| { | |
| if (priv->setClosed()) | |
| priv->close(); | |
| } | |
| void AsyncAppender::AsyncAppenderPriv::close() | |
| { | |
| this->stopDispatcher(); | |
| for (auto item : this->appenders.getAllAppenders()) | |
| { | |
| item->close(); | |
| } | |
| } | |
| AppenderList AsyncAppender::getAllAppenders() const | |
| { | |
| return priv->appenders.getAllAppenders(); | |
| } | |
| AppenderPtr AsyncAppender::getAppender(const LogString& n) const | |
| { | |
| return priv->appenders.getAppender(n); | |
| } | |
| bool AsyncAppender::isAttached(const AppenderPtr& appender) const | |
| { | |
| return priv->appenders.isAttached(appender); | |
| } | |
| bool AsyncAppender::requiresLayout() const | |
| { | |
| return false; | |
| } | |
| void AsyncAppender::removeAllAppenders() | |
| { | |
| priv->appenders.removeAllAppenders(); | |
| } | |
| bool AsyncAppender::removeAppender(const AppenderPtr& appender) | |
| { | |
| return priv->appenders.removeAppender(appender); | |
| } | |
| bool AsyncAppender::removeAppender(const LogString& n) | |
| { | |
| return priv->appenders.removeAppender(n); | |
| } | |
| bool AsyncAppender::replaceAppender(const AppenderPtr& oldAppender, const AppenderPtr& newAppender) | |
| { | |
| return priv->appenders.replaceAppender(oldAppender, newAppender); | |
| } | |
| void AsyncAppender::replaceAppenders( const AppenderList& newList) | |
| { | |
| priv->appenders.replaceAppenders(newList); | |
| } | |
| void AsyncAppender::setBufferSize(int size) | |
| { | |
| if (size < 0) | |
| { | |
| throw IllegalArgumentException(LOG4CXX_STR("size argument must be non-negative")); | |
| } | |
| std::lock_guard<std::mutex> lock(priv->dispatcherMutex); | |
| priv->bufferSize = (size < 1) ? 1 : size; | |
| } | |
| int AsyncAppender::getBufferSize() const | |
| { | |
| 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) | |
| { | |
| std::lock_guard<std::mutex> lock(priv->bufferMutex); | |
| priv->blocking = value; | |
| priv->bufferNotFull.notify_all(); | |
| } | |
| bool AsyncAppender::getBlocking() const | |
| { | |
| std::lock_guard<std::mutex> lock(priv->bufferMutex); | |
| return priv->blocking; | |
| } | |
| DiscardSummary::DiscardSummary(const LoggingEventPtr& event, const LogString& reasonArg) | |
| : maxEvent(event) | |
| , count(1) | |
| , reason(reasonArg) | |
| { | |
| } | |
| DiscardSummary::DiscardSummary(DiscardSummary&& other) | |
| : maxEvent(std::move(other.maxEvent)) | |
| , count(other.count) | |
| , reason(std::move(other.reason)) | |
| { | |
| } | |
| void DiscardSummary::add(const LoggingEventPtr& event) | |
| { | |
| if (this->maxEvent->getLevel()->toInt() < event->getLevel()->toInt()) | |
| this->maxEvent = event; | |
| ++this->count; | |
| } | |
| LoggingEventPtr DiscardSummary::createEvent() | |
| { | |
| LogString msg(LOG4CXX_STR("Discarded ")); | |
| StringHelper::toString(this->count, msg); | |
| msg.append(LOG4CXX_STR(" messages ") + this->reason + LOG4CXX_STR(" including: ")); | |
| msg.append(this->maxEvent->getRenderedMessage()); | |
| return std::make_shared<LoggingEvent> | |
| ( this->maxEvent->getLoggerName() | |
| , this->maxEvent->getLevel() | |
| , msg | |
| , LocationInfo::getLocationUnavailable() | |
| ); | |
| } | |
| void AsyncAppender::AsyncAppenderPriv::dispatch(const LogString& appenderName) | |
| { | |
| size_t discardCount = 0; | |
| size_t iterationCount = 0; | |
| size_t waitCount = 0; | |
| size_t producerBlockedCount = 0; | |
| int failureCount = 0; | |
| std::vector<size_t> pendingCountHistogram(this->buffer.size(), 0); | |
| bool isActive = true; | |
| while (isActive) | |
| { | |
| LoggingEventList events; | |
| 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) | |
| { | |
| ++waitCount; | |
| std::unique_lock<std::mutex> lock(this->bufferMutex); | |
| this->bufferNotEmpty.wait(lock, [this]() -> bool | |
| { return 0 < this->blockedCount || this->dispatchedCount != this->commitCount || this->closed; } | |
| ); | |
| } | |
| isActive = !this->isClosed(); | |
| while (events.size() < this->buffer.size() && this->dispatchedCount != this->commitCount) | |
| { | |
| auto index = this->dispatchedCount % this->buffer.size(); | |
| const auto& data = this->buffer[index]; | |
| events.push_back(data.event); | |
| if (data.pendingCount < pendingCountHistogram.size()) | |
| ++pendingCountHistogram[data.pendingCount]; | |
| ++this->dispatchedCount; | |
| } | |
| this->bufferNotFull.notify_all(); | |
| { | |
| std::lock_guard<std::mutex> lock(this->bufferMutex); | |
| producerBlockedCount += this->blockedCount; | |
| for (auto& discardItem : this->discardMap) | |
| { | |
| events.push_back(discardItem.second.createEvent()); | |
| discardCount += discardItem.second.getCount(); | |
| } | |
| 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 | |
| { | |
| this->appenders.appendLoopOnAppenders(item); | |
| } | |
| catch (std::exception& ex) | |
| { | |
| if (1 < ++failureCount) | |
| break; | |
| if (!this->isClosed()) | |
| this->errorHandler->error(LOG4CXX_STR("[") + appenderName + LOG4CXX_STR("] AsyncAppender"), ex, spi::ErrorCode::WRITE_FAILURE, item); | |
| } | |
| catch (...) | |
| { | |
| if (1 < ++failureCount) | |
| break; | |
| if (!this->isClosed()) | |
| this->errorHandler->error(LOG4CXX_STR("[") + appenderName + LOG4CXX_STR("] AsyncAppender unknown exception thrown")); | |
| } | |
| } | |
| ++iterationCount; | |
| } | |
| if (LogLog::isDebugEnabled()) | |
| { | |
| LogString msg(LOG4CXX_STR("[") + appenderName + LOG4CXX_STR("] AsyncAppender")); | |
| #ifdef _DEBUG | |
| msg += LOG4CXX_STR(" iterationCount "); | |
| StringHelper::toString(iterationCount, msg); | |
| msg += LOG4CXX_STR(" waitCount "); | |
| StringHelper::toString(waitCount, msg); | |
| msg += LOG4CXX_STR(" producerBlockedCount "); | |
| StringHelper::toString(producerBlockedCount, msg); | |
| msg += LOG4CXX_STR(" commitCount "); | |
| StringHelper::toString(this->commitCount, msg); | |
| #endif | |
| msg += LOG4CXX_STR(" dispatchedCount "); | |
| StringHelper::toString(this->dispatchedCount, msg); | |
| msg += LOG4CXX_STR(" discardCount "); | |
| StringHelper::toString(discardCount, msg); | |
| msg += LOG4CXX_STR(" pendingCountHistogram"); | |
| for (auto item : pendingCountHistogram) | |
| { | |
| msg += logchar(' '); | |
| StringHelper::toString(item, msg); | |
| } | |
| LogLog::debug(msg); | |
| } | |
| this->dispatcherActive = false; | |
| if (0 < this->blockedCount) // Restart this dispatcher? | |
| this->bufferNotFull.notify_all(); | |
| } |