Mantid
Loading...
Searching...
No Matches
PythonLoggingChannel.cpp
Go to the documentation of this file.
1// Mantid Repository : https://github.com/mantidproject/mantid
2//
3// Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
4// NScD Oak Ridge National Laboratory, European Spallation Source,
5// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
6// SPDX - License - Identifier: GPL - 3.0 +
7
8// local includes
10
11// 3rd-party includes
15#include <Poco/Message.h>
16#include <boost/python/errors.hpp>
17#include <boost/python/import.hpp>
18#include <boost/python/object.hpp>
19#include <deque>
20#include <limits>
21#include <mutex>
22#include <sstream>
23
24namespace Poco {
25
26namespace {
27// See https://docs.python.org/3/library/logging.html#logging-levels
28constexpr int PY_CRITICAL = 50;
29constexpr int PY_ERROR = 40;
30constexpr int PY_WARNING = 30;
31constexpr int PY_INFO = 20;
32constexpr int PY_DEBUG = 10;
33constexpr int PY_NOTSET = 0;
34constexpr size_t MAX_QUEUE_SIZE_DEFAULT = 10000;
35const std::string MAX_QUEUE_SIZE_KEY("logging.python.queueSize");
36
37auto pythonLevel(const Message::Priority prio) {
38 switch (prio) {
39 case Message::Priority::PRIO_FATAL:
40 case Message::Priority::PRIO_CRITICAL:
41 return PY_CRITICAL;
42 case Message::Priority::PRIO_ERROR:
43 return PY_ERROR;
44 case Message::Priority::PRIO_WARNING:
45 return PY_WARNING;
46 case Message::Priority::PRIO_NOTICE:
47 case Message::Priority::PRIO_INFORMATION:
48 return PY_INFO;
49 case Message::Priority::PRIO_DEBUG:
50 case Message::Priority::PRIO_TRACE:
51 return PY_DEBUG;
52 default:
53 return PY_NOTSET;
54 }
55}
56
60size_t configuredMaxQueueSize() {
61 auto &config = Mantid::Kernel::ConfigService::Instance();
62 // check first because getString emits a log message for a missing key, which would schedule another drain
63 if (!config.hasProperty(MAX_QUEUE_SIZE_KEY))
64 return MAX_QUEUE_SIZE_DEFAULT;
65
66 const auto value = config.getValue<int>(MAX_QUEUE_SIZE_KEY);
67 if (!value || *value < 0)
68 return MAX_QUEUE_SIZE_DEFAULT;
69 if (*value == 0)
70 return std::numeric_limits<size_t>::max();
71 return static_cast<size_t>(*value);
72}
73
74} // namespace
75
77 // The special behavior here is needed because Poco's LoggingFactory can be destroyed
78 // after the Python interpreter was shut down.
80 if (Py_IsInitialized()) {
82 // Destroy the object while the GIL is held.
83 pyLogger = nullptr;
84 } else {
85 // The Python interpreter has been shut down and our logger object destroyed.
86 // We can no longer safely call the destructor of *pyLogger,
87 // so just deallocate the memory.
88 operator delete(pyLogger.release());
89 }
90 }
91
92 std::mutex mutex;
93 std::deque<Message> queue;
94 std::unique_ptr<boost::python::object> pyLogger;
95 size_t maxQueueSize{MAX_QUEUE_SIZE_DEFAULT};
96 size_t droppedMessages{0};
97 bool callbackScheduled{false};
98 bool draining{false};
99 bool closed{false};
100};
101
104 auto logger = (boost::python::import("logging").attr("getLogger")("Mantid"));
105 m_state->pyLogger = std::make_unique<boost::python::object>(std::move(logger));
106}
107
109 try {
110 closeImpl();
111 } catch (...) {
112 // An exception must not escape the destructor. It cannot be reported through the logging framework that is
113 // being torn down, so any messages still queued are dropped.
114 }
115}
116
117void PythonLoggingChannel::log(const Poco::Message &msg) { enqueue(Poco::Message(msg)); }
118
119void PythonLoggingChannel::log(Poco::Message &&msg) { enqueue(std::move(msg)); }
120
121void PythonLoggingChannel::enqueue(Poco::Message &&msg) {
122 // Py_AddPendingCall cannot be used once the interpreter has been shut down
123 if (!Py_IsInitialized())
124 return;
125
126 bool scheduleCallback{false};
127 {
128 std::lock_guard lock(m_state->mutex);
129 if (m_state->closed)
130 return;
131
132 if (m_state->queue.size() < m_state->maxQueueSize)
133 m_state->queue.emplace_back(std::move(msg));
134 else
135 ++m_state->droppedMessages;
136
137 if (!m_state->callbackScheduled && !m_state->queue.empty()) {
138 m_state->callbackScheduled = true;
139 scheduleCallback = true;
140 }
141 }
142
143 if (!scheduleCallback)
144 return;
145
146 // The pending call keeps the state alive in case it runs after this channel has been destroyed
147 auto stateHolder = std::make_unique<std::shared_ptr<State>>(m_state);
148 // ownership is passed to pendingCallDrainQueue once the call is scheduled
149 auto *rawStateHolder = stateHolder.release();
150 if (Py_AddPendingCall(&PythonLoggingChannel::pendingCallDrainQueue, rawStateHolder) != 0) {
151 // the call was not scheduled, so take ownership back
152 stateHolder.reset(rawStateHolder);
153 std::lock_guard lock(m_state->mutex);
154 m_state->callbackScheduled = false;
155 }
156}
157
159 // Python runs pending calls on the main thread with the GIL held
160 const std::unique_ptr<std::shared_ptr<State>> holder(static_cast<std::shared_ptr<State> *>(stateHolder));
161 drainQueue(**holder);
162 return 0;
163}
164
166 // Refresh the limit on the Python thread so changes to the properties take effect without recreating the channel.
167 // Messages logged before the first drain use the default limit.
168 const auto maxQueueSize = configuredMaxQueueSize();
169
170 // The mutex is held asymmetrically on purpose. Producers (any C++ thread) hold it only to push a message, and this
171 // consumer holds it only to pop one; the call into Python below happens with the mutex released. This means a C++
172 // thread never waits on Python while logging, and a Python handler that logs back into Mantid re-enters enqueue()
173 // without deadlocking. pyLogger is not protected by the mutex because it is only used while holding the GIL.
174 //
175 // Calling into Python can run a pending drain on this thread (e.g. inside flush()). The nested drain returns
176 // immediately so that the outer loop delivers every message in order.
177 {
178 std::lock_guard lock(state.mutex);
179 if (state.draining)
180 return;
181 state.draining = true;
182 }
183
184 while (true) {
185 Message message;
186 size_t droppedMessages{0};
187 {
188 std::lock_guard lock(state.mutex);
189 state.maxQueueSize = maxQueueSize;
190 if (state.queue.empty()) {
191 state.callbackScheduled = false;
192 state.draining = false;
193 return;
194 }
195
196 message = std::move(state.queue.front());
197 state.queue.pop_front();
198 droppedMessages = state.droppedMessages;
199 state.droppedMessages = 0;
200 }
201
202 try {
203 const auto logFn = state.pyLogger->attr("log");
204 if (droppedMessages > 0) {
205 std::ostringstream warning;
206 warning << "PythonLoggingChannel dropped " << droppedMessages << " messages because its queue was full";
207 logFn(PY_WARNING, warning.str());
208 }
209 logFn(pythonLevel(message.getPriority()), message.getText());
210 } catch (boost::python::error_already_set &) {
211 PyErr_Print();
212 }
213 }
214}
215
217 if (!Py_IsInitialized())
218 return;
219
222}
223
225
226// m_state is never reset, so the C++ threads calling log() can read it while another thread closes the channel.
227// Messages logged after closing are discarded by the closed flag.
229 {
230 std::lock_guard lock(m_state->mutex);
231 m_state->closed = true;
232 }
233 flush();
234}
235} // namespace Poco
double value
The value of the point.
Definition FitMW.cpp:51
Defines a structure for acquiring/releasing the Python GIL using the RAII pattern.
const std::shared_ptr< State > m_state
Shared with any outstanding pending calls. It is set in the constructor and never reset.
void enqueue(Poco::Message &&msg)
static int pendingCallDrainQueue(void *stateHolder)
Callback with the signature required by Py_AddPendingCall.
void log(const Poco::Message &msg) override
static void drainQueue(State &state)
Deliver all queued messages to Python. The GIL must be held.
STL namespace.
std::unique_ptr< boost::python::object > pyLogger