HLCC Documentation 3.0.0
Loading...
Searching...
No Matches
ciiOldbDataPointAsync.ipp
Go to the documentation of this file.
1
18
19#include <chrono>
21
22
23namespace hlcc::oldbmux {
24
25//--------------------------------------------------------------+
26// operator<< |
27// |
28// The buffer DiscardListener currently logs the dropped value. |
29// For non-primitive types, we need a stream insertion operator |
30// matching the template type T. |
31// |
32// Is it OK to "pollute" namespace hlcc::oldbmux with these |
33// operator definitions? Is there a better solution? |
34// Perhaps to somehow recognize a vector type in the discard |
35// listener lambda impl and to iterate there? |
36// |
37// For more data types, should their insertion operators be |
38// added centrally here, or should the user provide them in |
39// client code? |
40//--------------------------------------------------------------+
41
42template <typename T>
43inline std::ostream& operator<<(std::ostream& os, const std::vector<T>& vec) {
44 os << "{";
45 for (std::size_t i = 0; i < vec.size(); ++i) {
46 os << vec[i];
47 if (i != vec.size() - 1) {
48 os << ", ";
49 }
50 }
51 os << "}";
52 return os;
53}
54
55
56//--------------------------------------------------------------+
57// CiiOldbDataPointAsync |
58// |
59//--------------------------------------------------------------+
60
61template<typename T>
63 std::string name,
64 std::shared_ptr<elt::oldb::CiiOldbDataPoint<T>> delegate,
65 boost::asio::thread_pool& async_exec,
66 const log4cplus::Logger& logger,
67 std::size_t buffer_capacity )
68 : m_sync_dtor_cv {},
69 m_sync_dtor_n_writing {0},
70 m_sync_dtor_mutex {},
71 logger {logger},
72 name {name},
73 delegate {delegate},
74 buffer {name, buffer_capacity, logger},
75 async_exec {async_exec},
76 is_async_processing {false}
77{
78
79 buffer.SetDiscardListener(
80 [this](OldbDataWithPromise& discarded_data) {
81
82 // Check the oldest remaining elements in the buffer, if it is incomplete. If our discarded_data has the missing fields,
83 // then they should be transferred to not get lost.
84 auto lock = buffer.Lock(); // should already be locked by CircularBufferConcurrent::Push or ::Close calling us, but we do it also here for clarity.
85
86 boost::circular_buffer<OldbDataWithPromise>& cb = buffer.GetCb();
87
88 // If the buffer is full, which is expected if a buffer.Push call got us here
89 // (as opposed to a buffer.Close call where a full buffer would be coindicental),
90 // and the retained data has missing attributes that are present in the older to-be-discarded data,
91 // then we transfer these attributes. For that we peek into the internal buffer to check the retained data.
92 // We exceptionally use a raw pointer, which serves both as a flag for this situation and for data access.
93 // std::optional does not work here because we have only a reference to the retained data, cannot move it out.
94 OldbDataWithPromise* oldest_retained_data = ( cb.full() ? &cb.front() : nullptr );
95
96 std::ostringstream msg {};
97
98 if (discarded_data.GetValue()) {
99 msg << "value=" << *discarded_data.GetValue();
100 if (oldest_retained_data && !oldest_retained_data->GetValue()) {
101 oldest_retained_data->SetValue(*discarded_data.GetValue());
102 msg << " (transferred to retained newer data)";
103 }
104 msg << ", ";
105 }
106
107 if (discarded_data.GetTimestamp()) {
108 msg << "timestamp=" << *discarded_data.GetTimestamp();
109 if (oldest_retained_data && !oldest_retained_data->GetTimestamp()) {
110 oldest_retained_data->SetTimestamp(*discarded_data.GetTimestamp());
111 msg << " (transferred to retained newer data)";
112 }
113 msg << ", ";
114 }
115
116 if (discarded_data.GetQuality()) {
117 std::string quality_str = hlcc::cpputil::CiiOldbDpQualityToString(*discarded_data.GetQuality());
118 msg << "quality=" << quality_str;
119 // no need to transfer, since data produced with WriteValue has quality default value,
120 // and data produced with SetQuality anyway has the quality.
121 }
122 LOG4CPLUS_DEBUG(this->logger, "OLDB async (" << this->name << ") discarded data: " << msg.str());
123
124 // If a DP value was discarded from the buffer, we need to notify the client through its future object.
125 // TODO: Is there a standardized (std or ELT etc) exception for cancelled operations? std::out_of_range does not really fit..
126 // TODO use std::make_exception_ptr if we use std::promise instead of boost.
127 discarded_data.GetPromise()->set_exception(boost::copy_exception(std::out_of_range{"Dropped data."}));
128
129 std::lock_guard lck {this->m_sync_dtor_mutex};
130 this->m_sync_dtor_n_writing--;
131 });
132}
133
134
135template<typename T>
137 RAD_TRACE(logger);
138
139 // notify/release pool thread in Poll method, if any.
140 buffer.Close();
141
142
143 // We lock m_sync_dtor_mutex after calling Close(), because Close() calls the discard handler
144 // that locks m_sync_dtor_mutex for a short time.
145 // What matters is that we lock m_sync_dtor_mutex before calling m_sync_dtor_cv.wait.
146 // It is the responsibility of the application to not call write methods when the dtor
147 // is running, thus no need to lock m_sync_dtor_mutex at the beginning of the dtor.
148 std::unique_lock lck {m_sync_dtor_mutex};
149
150 // Block dtor (if needed) until pool thread has left WriteBufferToOldb.
151 // We must not have this CiiOldbDataPointAsync object destroyed while another thread is using it.
152 // Note that we cannot simply join the external pool thread, because it lives beyond this object.
153 // There are also no callbacks offered by the boost thread pool that would tell us when a task has finished.
154 m_sync_dtor_cv.wait(lck, [this]{
155 LOG4CPLUS_TRACE(logger, "~CiiOldbDataPointAsync (" << name << ") m_sync_dtor_cv.wait predicate function: "
156 << "m_sync_dtor_n_writing=" << m_sync_dtor_n_writing);
157 return (m_sync_dtor_n_writing == 0);
158 });
159
160 LOG4CPLUS_TRACE(logger, "~CiiOldbDataPointAsync (" << name << ") is done.");
161}
162
163
164template<typename T>
165std::shared_ptr<elt::oldb::CiiOldbDpValue<T>> CiiOldbDataPointAsync<T>::ReadValue(bool check_bad_quality) {
166 return delegate->ReadValue(check_bad_quality);
167}
168
169
176template<typename T>
177boost::future<typename CiiOldbDataPointAsync<T>::OldbData> CiiOldbDataPointAsync<T>::WriteValue(
178 const T& value, int64_t timestamp, elt::oldb::CiiOldbDpQuality quality, bool is_disable_publishing) {
179
180 { // lock scope
181 // If this write call is made before the dtor runs, then it will block the dtor.
182 // The reverse situation is illegal: No new write calls must be made once the dtor runs.
183 std::lock_guard lck {m_sync_dtor_mutex};
184 m_sync_dtor_n_writing++;
185 }
186 // Note that we store a copy of value in new_data. Thus no lifetime issues with our "T& value" parameter.
187 typename CiiOldbDataPointAsync<T>::OldbData new_data {value, timestamp, quality};
188 return CiiOldbDataPointAsync<T>::WriteAsync(std::move(new_data), is_disable_publishing);
189}
190
191
192template<typename T>
193boost::future<typename CiiOldbDataPointAsync<T>::OldbData> CiiOldbDataPointAsync<T>::SetQuality(
194 elt::oldb::CiiOldbDpQuality quality, bool is_disable_publishing) {
195
196 { // lock scope
197 std::lock_guard lck {m_sync_dtor_mutex};
198 m_sync_dtor_n_writing++;
199 }
200 typename CiiOldbDataPointAsync<T>::OldbData new_data {quality};
201 return CiiOldbDataPointAsync<T>::WriteAsync(std::move(new_data), is_disable_publishing);
202}
203
204
205template<typename T>
206boost::future<typename CiiOldbDataPointAsync<T>::OldbData> CiiOldbDataPointAsync<T>::WriteAsync(
207 OldbData&& new_data, bool is_disable_publishing) {
208
209 // TODO: If specific write methods get added in the future, they all must call
210 // m_sync_dtor_n_writing++
211 // the same way that WriteValue and SetQuality do.
212
213 std::chrono::steady_clock::time_point t_0 {std::chrono::steady_clock::now()};
214
215 // We need a future/promise pair that is separate from any future we may obtain from running a thread.
216 // The reasons are
217 // (a) our lossy buffer, where we must "cancel" the future toward the client also
218 // when the data gets evicted from the buffer, without having attempted to write it to OLDB.
219 // See the SetDiscardListener call in our ctor.
220 // (b) the optimization that we only push the data to the buffer without starting a thread
221 // when there is already another thread busy with moving data from the buffer to the OLDB.
222 // The future will inform the client about any success or failure in writing the data to OLDB.
223 boost::promise<OldbData> promise {};
224 boost::future<OldbData> future = promise.get_future();
225
226 // TODO: handle is_disable_publishing
227
228 std::chrono::steady_clock::time_point t_1 {std::chrono::steady_clock::now()};
229
230 typename CiiOldbDataPointAsync<T>::OldbDataWithPromise new_data_with_promise {
231 // no std::move on the value, otherwise warning "moving a temporary object prevents copy elision"
232 new_data.GetValue(), new_data.GetTimestamp(), new_data.GetQuality(), std::move(promise)};
233
234 // Write data to the circular buffer.
235 // This discards older data if the buffer is full.
236 // Once the data is in the buffer, it may be processed immediately by a background
237 // thread that was started for the previously pushed data.
238 buffer.Push(std::move(new_data_with_promise));
239
240 std::chrono::steady_clock::time_point t_2 {std::chrono::steady_clock::now()};
241
242 if (new_data.GetValue()) {
243 LOG4CPLUS_TRACE(logger, "OLDB async (" << name << ") wrote to buffer: "
244 << *new_data_with_promise.GetValue() << ", threadId=" << std::this_thread::get_id());
245 } else {
246 LOG4CPLUS_TRACE(logger, "OLDB async (" << name << ") wrote to buffer: <no value>"
247 << ", threadId=" << std::this_thread::get_id());
248 }
249
250 // Trigger background processing of buffered data, unless we are already processing.
251 bool expected = false; // TODO: how to pass bool& as a 'false' literal, without declaring the variable?
252
253 std::chrono::steady_clock::time_point t_3 {};
254 std::chrono::steady_clock::time_point t_4 {};
255
256 // if is_async_processing == false then set it to true and execute the code block that triggers background processing.
257 if (is_async_processing.compare_exchange_strong(expected, true)) {
258
259 LOG4CPLUS_TRACE(logger, LOG4CPLUS_TEXT("OLDB async (") << name << LOG4CPLUS_TEXT(") will use a writer thread."));
260
261 // Write the buffer content to the OLDB, using sync CII OLDB client API calls in a separate thread.
262 // New data can come in concurrently and will be included. The thread finishes when the buffer is fully drained.
263 //
264 // We use a shared thread pool to cut thread creation overhead and to centrally synchronize
265 // on thread completion during application shutdown (otherwise segfaults, see ETCS-629).
266 //
267 // Note that the externally managed thread pool resolves also an issue that we would otherwise have with using
268 // std::async. There would be the "waiting destructor" issue of the special version of future returned,
269 // see https://en.cppreference.com/w/cpp/thread/future/~future. The returned future
270 // runs out of scope before we return from this method. Thus this method itself waits for
271 // the background thread execution to finish, making OLDB access an unwanted sync call.
272 // This problem almost caused deprecation of std::async, see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3780.pdf
273 // With std::thread we would have the same scope and blocking issue, unless we externally manage the single thread
274 // or we detach the thread. But then the C++ standard does not define what happens with detached threads when
275 // the application exits, and we cannot sync application shutdown on the detached thread. All messed up.
276 //
277 // Note on the number of threads and the task queue: In the Java prototype we used infinite maximumPoolSize
278 // (well, Integer.MAX_VALUE). Here in C++ with the much larger thread stack size and possibly other thread
279 // overhead, we use a quite limited number of threads.
280 // OLDB's underlying Redis is single-threaded per instance. The ECM installation uses 6 Redis instances,
281 // so that our 5 threads seem reasonable also from that perspective.
282 // The finite thread pool has an unlimited task queue in front of it.
283 // This should ensure that we return quickly from the 'post' call (where in reality it is sometimes very slow!)
284 // If new data arrives while a thread is already writing to OLDB, it will also include the new data.
285 // Thus it may happen that another thread will later find an empty data buffer. That should be almost no overhead
286 // thanks to reusing threads from a pool, and on the other hand it ensures that even when all threads are busy,
287 // no data will starve in the buffer.
288
289 t_3 = std::chrono::steady_clock::now();
290
291 // The lambda function helps to wrap our non-static member function WriteBufferToOldb as a std::function.
292 boost::asio::post(async_exec, [this](){ WriteBufferToOldb(); });
293
294 t_4 = std::chrono::steady_clock::now();
295 } else {
296 t_3 = t_4 = std::chrono::steady_clock::now();
297 // There is already a thread draining the buffer (writing to OLDB).
298 // Our data that we've pushed into the buffer will be considered by that other thread.
299 LOG4CPLUS_TRACE(logger, LOG4CPLUS_TEXT("OLDB async (") << name << LOG4CPLUS_TEXT(") will rely on running writer thread."));
300 }
301
302 LOG4CPLUS_TRACE(logger, "OLDB async (" << name << ") Returning from WriteAsync");
303
304 int d1 = std::chrono::duration_cast<std::chrono::microseconds>(t_1 - t_0).count();
305 int d2 = std::chrono::duration_cast<std::chrono::microseconds>(t_2 - t_1).count();
306 int d3 = std::chrono::duration_cast<std::chrono::microseconds>(t_3 - t_2).count();
307 int d4 = std::chrono::duration_cast<std::chrono::microseconds>(t_4 - t_3).count();
308
309 if (d1 + d2 + d3 + d4 > 1000) {
310 LOG4CPLUS_DEBUG(logger, "CiiOldbDataPointAsync " << name << " WriteAsync steps in micros: "
311 << d1 << ", " << d2 << ", " << d3 << ", " << d4);;
312 }
313 return future; // compiler will do copy elision or implied std::move
314}
315
316
317template<typename T>
318void CiiOldbDataPointAsync<T>::WriteBufferToOldb() {
319 LOG4CPLUS_TRACE(logger, LOG4CPLUS_TEXT("OLDB async (") << name << LOG4CPLUS_TEXT(") writer thread started. is_async_processing=") << is_async_processing <<
320 ", threadId=" << std::this_thread::get_id()); // note that the remote logs contain threadId also in the "SourceID" field.
321
322 int write_count = 0;
323
324 // Fetch data, but without waiting.
325 // Normally there should be data available, as otherwise the worker thread would not be running here.
326 std::optional<OldbDataWithPromise> data_to_write_opt {buffer.Poll()};
327
328 if (!data_to_write_opt) {
329 // There is the rare case that method WriteValue pushed new data to the buffer while a background thread
330 // from a previous write was still draining the buffer, but that background write finished before method WriteValue
331 // checked variable is_async_processing. Then we get here with an empty buffer.
332 // All we need to do is restore this flag.
333 is_async_processing = false;
334 } else {
335 // Process new data as it gets added to the buffer (possibly also replacing older data, while we write).
336 // Once the destructor runs, we just finish current data, but discard queued data.
337 while (data_to_write_opt.has_value()) {
338
339 LOG4CPLUS_TRACE(logger, LOG4CPLUS_TEXT("OLDB async (") << name << ") thread got data and will call the sync writeValue.");
340
341 OldbDataWithPromise data_to_write {std::move(data_to_write_opt.value())};
342
343 try {
344 auto start = std::chrono::steady_clock::now(); // TODO do we have a Stopwatch utility class?
345
346 // make the synchronous OLDB call
347 if (data_to_write.GetValue() && data_to_write.GetTimestamp() && data_to_write.GetQuality()) {
348 delegate->WriteValue(*data_to_write.GetValue(), *data_to_write.GetTimestamp(), *data_to_write.GetQuality(), false);
349 LOG4CPLUS_TRACE(logger, LOG4CPLUS_TEXT("OLDB async (") << name <<
350 LOG4CPLUS_TEXT(") wrote DP to OLDB in ") << std::chrono::duration_cast<std::chrono::milliseconds>((std::chrono::steady_clock::now() - start)).count() <<
351 " ms, data=" << LOG4CPLUS_TEXT(*data_to_write.GetValue()) ); // TODO log also time and quality?
352 }
353 else if (data_to_write.GetQuality()) {
354 delegate->SetQuality(*data_to_write.GetQuality());
355 LOG4CPLUS_TRACE(logger, "OLDB async (" << name <<
356 ") wrote DP quality to OLDB in " << std::chrono::duration_cast<std::chrono::milliseconds>((std::chrono::steady_clock::now() - start)).count() <<
357 " ms, quality=" << hlcc::cpputil::CiiOldbDpQualityToString(*data_to_write.GetQuality()) );
358 }
359 else {
360 LOG4CPLUS_DEBUG(logger, LOG4CPLUS_TEXT("Programming error: OLDB async (") << name <<
361 ") could not write DP because of missing data in OldbDataWithPromise");
362 }
363
364 data_to_write.GetPromise()->set_value(std::move(data_to_write));
365 write_count++;
366 } catch (const elt::oldb::CiiOldbException& ex) {
367 LOG4CPLUS_DEBUG(logger, LOG4CPLUS_TEXT("OLDB async (") << name << ") writing to OLDB failed: " << ex.what());
368 try {
369 // store exception in the promise/future
370 data_to_write.GetPromise()->set_exception(boost::copy_exception(ex));
371 } catch(...) {} // set_exception() may throw too
372 } catch (...) {
373 // TODO: should we let the application crash, or also set an ex on the promise?
374 LOG4CPLUS_WARN(logger, LOG4CPLUS_TEXT("OLDB async (") << name << ") unknown exception.");
375 }
376
377 // Check if there is more data in the buffer that needs to be written to OLDB.
378 // We lock the buffer so that toggling of flag is_async_processing is safe from
379 // concurrent Push calls, which might otherwise lead to data starving in the buffer.
380 { // scope for the lock
381 auto lock = buffer.Lock();
382 data_to_write_opt = buffer.Poll();
383 if (!data_to_write_opt.has_value()) {
384 is_async_processing.store(false); // from here on, newly written data will call WriteBufferToOldb again.
385 // we'll exit from the while loop.
386 }
387 }
388 }
389 LOG4CPLUS_TRACE(logger, LOG4CPLUS_TEXT("OLDB async (") << name <<
390 LOG4CPLUS_TEXT(") performed ") << write_count << LOG4CPLUS_TEXT(" DP writes in the background, now releasing writer thread."));
391 }
392
393 // Decrement the n_writing flag and notify the CV, in case the dtor is waiting on it.
394 // TODO: Should we wrap the code above in a try, and the code below in a catch(...), or is all critical code already in try-catch?
395 // The risk with an excpetion is to not decrement m_sync_dtor_n_writing and thus starve the dtor (until spurious wakeup).
396 std::lock_guard lck {m_sync_dtor_mutex};
397 m_sync_dtor_n_writing -= write_count;
398 LOG4CPLUS_TRACE(logger, "OLDB async (" << name << ") WriteBufferToOldb done. m_sync_dtor_n_writing=" << m_sync_dtor_n_writing);
399 // We notify while under the lock, to hold off a loitering dtor until this worker thread has ended in peace.
400 m_sync_dtor_cv.notify_all();
401}
402
403} // namespace hlcc::oldbmux
Requestor class header file.
CiiOldbDataPointAsync(std::string name, std::shared_ptr< elt::oldb::CiiOldbDataPoint< T > > delegate, boost::asio::thread_pool &async_exec, const log4cplus::Logger &logger, std::size_t buffer_capacity=1)
Constructor.
Definition ciiOldbDataPointAsync.ipp:62
std::shared_ptr< elt::oldb::CiiOldbDpValue< T > > ReadValue(bool check_bad_quality=true)
Definition ciiOldbDataPointAsync.ipp:165
boost::future< typename CiiOldbDataPointAsync< T >::OldbData > WriteValue(const T &value, int64_t timestamp=elt::oldb::CiiOldbUtil::Now(), elt::oldb::CiiOldbDpQuality quality=elt::oldb::CiiOldbDpQuality::OK, bool is_disable_publishing=false)
Definition ciiOldbDataPointAsync.ipp:177
boost::future< typename CiiOldbDataPointAsync< T >::OldbData > SetQuality(elt::oldb::CiiOldbDpQuality quality, bool is_disable_publishing=false)
Definition ciiOldbDataPointAsync.ipp:193
virtual ~CiiOldbDataPointAsync()
Definition ciiOldbDataPointAsync.ipp:136
std::string CiiOldbDpQualityToString(const elt::oldb::CiiOldbDpQuality &quality)
Definition ciiTypesToString.cpp:9
elt::mal::future< T > future
Definition actionsCommands.cpp:103
Definition ciiOldbDataPointAsync.hpp:35
std::ostream & operator<<(std::ostream &os, const std::vector< T > &vec)
Definition ciiOldbDataPointAsync.ipp:43
Value type for data for buffering and writing to OLDB. This subclass is used internally by CiiOldbDat...
Definition ciiOldbDataPointAsync.hpp:288
boost::promise< CiiOldbDataPointAsync< T >::OldbData > * GetPromise()
Definition ciiOldbDataPointAsync.hpp:306
Value type for data for buffering and writing to OLDB. This base class is used as an interface toward...
Definition ciiOldbDataPointAsync.hpp:215
void SetTimestamp(int64_t timestamp)
Definition ciiOldbDataPointAsync.hpp:257
std::optional< T > GetValue() const
Definition ciiOldbDataPointAsync.hpp:248
std::optional< int64_t > GetTimestamp() const
Definition ciiOldbDataPointAsync.hpp:254
std::optional< elt::oldb::CiiOldbDpQuality > GetQuality() const
Definition ciiOldbDataPointAsync.hpp:260
void SetValue(T value)
Definition ciiOldbDataPointAsync.hpp:251