Flow 3.0.0
Flow project: Full implementation reference.
thread_lcl_str_appender.hpp
Go to the documentation of this file.
1/* Flow
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
24#include <boost/iostreams/device/array.hpp>
25#include <boost/iostreams/stream.hpp>
26#include <boost/iostreams/stream_buffer.hpp>
27#include <boost/io/ios_state.hpp>
28#include <boost/move/unique_ptr.hpp>
29#include <boost/unordered/unordered_flat_map.hpp>
30#include <boost/thread.hpp>
31#include <string>
32
33namespace flow::log
34{
35
36/**
37 * Internal flow::log class that facilitates a more efficient way to get util::ostream_op_to_string() behavior
38 * by allowing each thread to repeatedly reuse the structures that function creates from scratch on stack each time it
39 * is invoked; furthermore each logging entity is allotted a separate such set of structures to enable each entity to
40 * not affect the streams of other entities.
41 *
42 * The problem statement arises from util::ostream_op_to_string()'s only obvious flaw which is performance.
43 * One invokes that function and passes a target `std::string` and a sequence of `ostream`-writing operations. Usually
44 * the `string` has just been newly created `{` locally `}`; and the function itself will ultimately create some Boost
45 * `iostreams` machinery `{` locally `}` and then use it to efficiently write through that machinery into the `string`.
46 * That's all fine, but if the function is very often invoked, it would be nice
47 * to instead reuse a global `string` *and* associated machinery between invocations. (`ostream_op_to_string()` can
48 * reuse an existing `string`, but even then it'll need to re-create the `iostreams` machinery each time.)
49 * In particular, logging machinery (`FLOW_LOG_*()` especially) indeed very often invokes such behavior and could thus
50 * benefit (hence the inspiration behind this class).
51 *
52 * Well, using global structures is not OK: multiple threads will corrupt any such structures by writing and/or
53 * reading concurrently to/from them. However, if one creates a structure per thread -- a/k/a a thread-local
54 * structure -- then that danger goes away completely. That is the principle behind this class. Firstly, from the
55 * class user's perspective, the class itself is a singleton per thread. Call this_thread_string_appender() to
56 * access the current thread's only instance of Thread_local_string_appender. Now you have a pointer to that
57 * object. Further use is very simple: fresh_appender_ostream() clears the internally stored target string and
58 * returns a pointer to an `ostream` object. Write to this `ostream` like any other `ostream`. Once done doing so,
59 * call target_contents(), which will return a reference to the read-only string which has been appended to with
60 * the aforementioned `ostream` writing. Repeat each time you'd like to write to a string, and then quickly access
61 * that string.
62 *
63 * @warning this_thread_string_appender() may return null. If this happens, it means we are near thread
64 * death, and the required thread-local data we keep has been deinitialized (destroyed). The caller
65 * must have a contingency plan in this case; the appropriate stream simply no longer exists, nor can
66 * it reasonably exist. However, for the contingency, it is possible to directly instantiate a
67 * `*this` (which is why the ctor is public).
68 *
69 * Update: A further level of indirection/lookup was added when I realized that, for each thread, it's not desirable
70 * that logically separate stream writers (typically each allotted its own writing object, e.g., a Logger) all write
71 * to one common stream. Even if their writes are not interleaved within an "atomic" sequence of characters,
72 * state-changing `ostream` formatters (`std::hex` and the like) would cause one writer to affect the formatting of
73 * another, which is not desired (though maybe not immediately obvious due to formatting being a relatively rarely
74 * used feature for many). Therefore, each writing "entity" is allotted a separate instance of this class, and
75 * lookup is (implicitly) via thread ID and (explicitly) via a source object ID which uniquely (over all time)
76 * identifies each object.
77 *
78 * ~~~
79 * // If you frequently do this, from one or from multiple threads:
80 * class Distinct_writer
81 * {
82 * ...
83 * string output;
84 * // Expensive structure setup occurs before the stream writing can execute. (Terminating `flush` is assumed.)
85 * util::ostream_op_to_string(&output, "The answer is: [", std::hex, 42, "].");
86 * log_string(output); // Suppose log_string() takes const std::string&.
87 * log_chars(output.c_str()); // Or suppose log_chars() takes const char*.
88 * // std::hex formatting is forgotten, as each util::ostream_op_to_string() creates a new everything.
89 * ...
90 * }
91 *
92 * // Then consider replacing it with this (for better performance over time):
93 * class Distinct_writer : private util::Unique_id_holder
94 * {
95 * ...
96 * // Note lookup by `this`: we are also a Unique_id_holder which means this can get our unique ID.
97 * auto const appender = log::Thread_local_string_appender::this_thread_string_appender(*this);
98 * // Note added explicit `flush`.
99 * *(appender->fresh_appender_ostream()) << "The answer is: [" << std::hex << 42 << "]." << std::flush;
100 * log_string(appender->target_contents());
101 * log_chars(appender->target_contents().c_str());
102 * // Additional feature: std::hex formatting will persist for further such snippets, for the current thread.
103 * ...
104 * }
105 * ~~~
106 *
107 * Note, once again, that because this_thread_string_appender() returns a thread-local object, it is by definition
108 * impossible to corrupt anything inside it due to multiple threads writing to it. (That is unless, of course, you
109 * try passing that pointer to another thread and writing to it there, but that's basically malicious behavior;
110 * so don't do that.)
111 *
112 * ### Thread safety ###
113 * Well, see above. If you use class as prescribed, then it's safe to read and write without locking around
114 * object of this class.
115 *
116 * ### Memory use ###
117 * The object itself is not large, storing some streams and ancillary stream-related obejcts.
118 * The lookup table just indexes by object ID and thread ID, so a few integers per writing entity per writing thread.
119 * The implementation we use ensures that once a given thread disappears, all the data for that thread ID are freed.
120 * Let us then discuss, from this point forth, what happens while a given thread is alive. Namely:
121 *
122 * There is no way to remove an appender from this table once it has been added which can be seen as a
123 * memory leak. In practice, the original use case of Logger means the number of lookup table entries is not likely
124 * to grow large enough to matter -- but extreme scenarios contradicting this estimation may be contrived or occur.
125 *
126 * In terms of implementation, getting cleanup to work is extremely difficult and possibly essentially impossible.
127 * (This is true because it's not possible, without some kind of undocumented and possibly un-portable hackery, to
128 * enumerate all the threads from which a given object has created a Thread_local_string_appender. Even if one could,
129 * it is similarly not really possible to do anything about it without entering each such thread, which is quite hard
130 * to do elegantly and unintrusively w/r/t to calling code. Even if one could enter each such thread, some
131 * kind of locking would probably need to be added, eliminating the elegance of the basic premise of the class which
132 * is that of a thread-local singleton per source object.) For this reason, adding cleanup is not even listed as a
133 * to-do.
134 *
135 * (Update: util::Thread_local_state_registry now exists and can probably be used to achieve the above. At any rate
136 * it deals with just such things. That does not mean it is worthwhile to use it for *this* though.)
137 *
138 * Since util::Unique_id_holder is unique over all time, not just at any given time, there is no danger that the reuse
139 * of some dead object's ID will cause a collision. Historically this was a problem when we used `this` pointers
140 * as IDs (as once an object is gone, its `this` value can be reused by another, new object).
141 *
142 * ### Where is the `thread_local`? ###
143 * There is no `static thread_local` member for the #Source_obj_to_appender_map, which is the master data
144 * structure, so what's goign on -- one might ask, if one does not dive into the impl functions. Answer:
145 * We use what is conceptually a `static thread_local`-replacer/augmenter: Thread_local_obj_deinit_safe; so
146 * instead of, like, `s_this_thread_appender_ptrs[so_and_so]...` (where the `s_` would have been a `static thread_local`
147 * member of the class) it is instead:
148 *
149 * ~~~
150 * const auto this_thread_appender_ptrs
151 * = Thread_local_obj_deinit_safe<Source_obj_to_appender_map, Thread_local_string_appender>
152 * ::this_thread_obj_or_null();
153 * if (this_thread_appender_ptrs)
154 * {
155 * (*this_thread_appender_ptrs)[so_and_so]...
156 * }
157 * else
158 * {
159 * // No good. We are near thread death, and our thread_local map has been deinitialized already.
160 * }
161 * ~~~
162 *
163 * The latter case <=> this_thread_string_appender() returns null.
164 */
166 private boost::noncopyable
167{
168public:
169 // Constructors/destructor.
170
171 /**
172 * Initializes object with an empty string and the streams machinery available to write to that string.
173 *
174 * @warning It is not intended for direct use in most cases; rather it was made public to help with
175 * the contingency when this_thread_string_appender() returns null (near thread exit).
176 */
178
179 // Methods.
180
181 /**
182 * Returns a pointer to the exactly one Thread_local_string_appender object that is accessible from
183 * the current thread for the given source object; or null if the thread is near death, and this
184 * Thread_local_string_appender facility is no longer available for use.
185 *
186 * @note Be ready for the null return. In particular it is in danger of occurring if and only if one
187 * accesses us from a destructor (or some other form of cleanup function) of a `thread_local` or otherwise
188 * thread-local (Thread_local_ptr, Thread_local_state_registry, `thread_specific_ptr`...) object.
189 *
190 * The source object is given by its ID. The source object can store or contain (or be otherwise mapped
191 * to) its own util::Unique_id_holder; and thus it can be
192 * any object (e.g., a Logger, in original use case) that desires the use of one distinct (from other
193 * objects) but continuous (meaning any stream state including characters output will persist over time)
194 * `ostream`. (The `ostream` is not returned directly but rather as the wrapping Thread_local_string_appender
195 * object for that stream.) Ultimately there is exactly one `ostream` (and Thread_local_string_appender) per
196 * (invoking thread T, `source_obj_id`) pair that has invoked the present method so far, including the current
197 * invocation itself.
198 *
199 * @param source_obj_id
200 * An ID attached in a 1-to-1 (over all time until program exit) fashion to the entity (typically, class
201 * instance of any type, e.g., Logger) desiring its own Thread_local_string_appender.
202 * @return See above. Again: note that near thread death it may return null.
203 */
205
206 /**
207 * Clears the internally stored string (accessible for reading via target_contents()), and returns an
208 * output stream writing to which will append to that string. You must `flush` (or flush any other way like
209 * `endl`) the stream in order to ensure characters are actually written to the string. (I am fairly sure flushing
210 * is in fact THE thing that actually writes to the string.)
211 *
212 * The pointer's value never changes for `*this` object. This fact is critical when it comes to the logic
213 * of sequential save_formatting_state_and_restore_prev() calls, as well as (for example) the `ostream` continuity
214 * semantics described in class Logger header (that class uses the present class).
215 *
216 * Behavior is undefined if, at the time the present method is called, the last `flush` of
217 * `*fresh_appender_ostream()` precedes the last writing of actual data to same. In other words,
218 * always `flush` the stream immediately after writing to it, or else who knows what will happen
219 * with any un-flushed data when one subsequently calls fresh_appender_ostream(), clearing the string?
220 *
221 * Behavior is undefined if you call this from any thread other than the one in which
222 * this_thread_string_appender() was called in order to obtain `this`.
223 *
224 * @see save_formatting_state_and_restore_prev() is a valuable technique to enable usability in
225 * user-facing APIs that use Thread_local_string_appender within the implementation (notably logging
226 * APIs). See its doc header before using this method.
227 *
228 * @return Pointer to `ostream` writing to which (followed by flushing) will append to string that
229 * is accessible via target_contents().
230 */
231 std::ostream* fresh_appender_ostream();
232
233 /**
234 * Same as fresh_appender_ostream() but does not clear the result string, enabling piecemeal writing to
235 * that string between clearings of the latter.
236 *
237 * @return Identical to fresh_appender_ostream()'s return value.
238 */
239 std::ostream* appender_ostream();
240
241 /**
242 * Saves the formatting state of the `ostream` returned by appender_ostream() and sets that same `ostream`
243 * to the state saved last time this method was called on `*this`, or at its construction, whichever happened
244 * later. Examples of formatting state are `std::hex` and locale imbuings.
245 *
246 * This is useful, if you tend to follow a pattern like the following
247 * macro definition that takes `user_supplied_stream_args` macro argument:
248 *
249 * ~~~
250 * *(appender->fresh_appender_ostream())
251 * << __FILE__ << ':' << __LINE << ": " // First standard prefix to identify source of log line...
252 * << user_supplied_stream_args // ...then user-given ostream args (token may expand to multiple <<s)...
253 * << '\n' // ...then any terminating characters...
254 * << std::flush; // ...and ensure it's all flushed into `string target_contents()`.
255 * ~~~
256 *
257 * If this is done repeatedly, then `user_supplied_stream_args` might include formatting changes like `std::hex`.
258 * In this example, there is the danger that such an `std::hex` from log statement N would then "infect"
259 * the `__LINE__` output from log statement (N + 1), the line # showing up in hex form instead of decimal.
260 * However, if you use the present feature, then the problem will not occur.
261 * However, to avoid similarly surprising the user, restore their formatting with the same call.
262 * The above example would become:
263 *
264 * ~~~
265 * auto& os = *appender->fresh_appender_ostream();
266 * os.save_formatting_state_and_restore_prev(); // Restore pristine formatting from constuction time.
267 * os << __FILE__ << ':' << __LINE << ": "; // Can log prefix without fear of surprising formatting.
268 * // (Note that if apply formatters here for more exotic output, undo them before the following call.)
269 * os.save_formatting_state_and_restore_prev(); // Restore formatting from previous user_supplied_stream_args.
270 * os << user_supplied_stream_args
271 * << '\n' // Formatting probably doesn't affect this, so no need to worry about restoring state here.
272 * << std::flush;
273 * ~~~
274 *
275 * @note Reminder: All access to a `*this` must occur in one thread by definition. Therefore there are no
276 * thread safety concerns to do with the suggested usage patterns.
277 * @note Detail: The formatting features affected are as described in documentation for
278 * `boost::io::basic_ios_all_saver`. This can be summarized as everything except user-defined formatters.
279 */
281
282 /**
283 * Read-only accessor for the contents of the `string`, as written to it since the last fresh_appender_ostream()
284 * call, or object construction, whichever occurred later.
285 *
286 * The reference's value never changes for `*this` object. The string's value may change depending on
287 * whether the user writes to `*fresh_appender_ostream()` (which writes to the string) or calls that
288 * method (which clears it).
289 *
290 * Behavior is undefined if you call this from any thread other than the one in which
291 * this_thread_string_appender() was called in order to obtain `this`.
292 *
293 * ### Rationale ###
294 * Why return `const string&` instead of util::String_view? Answer: Same as in doc header of String_ostream::str().
295 *
296 * @return Read-only reference to `string`.
297 */
298 const std::string& target_contents() const;
299
300private:
301
302 // Types.
303
304 /**
305 * Short-hand for map of a given thread's appender objects indexed by the IDs of their respective source objects.
306 * Smart pointers are stored to ensure the Thread_local_string_appender is deleted once removed from such a map.
307 * Thus once a per-thread map disappears at thread exit, all the stored objects within are freed also.
308 */
310 = boost::unordered_flat_map<util::Unique_id_holder::id_t,
311 boost::movelib::unique_ptr<Thread_local_string_appender>>;
312
313 // Data.
314
315 // (No `static thread_local Source_obj_to_appender_map`: see class doc header for explanation.)
316
317 /// The target string wrapped by an `ostream`. Emptied at construction and in fresh_appender_ostream() *only*.
319
320 /**
321 * Stores the `ostream` formatter state from construction time or time of last
322 * save_formatting_state_and_restore_prev(), whichever occurred later; clearing (including as part of reassigning)
323 * this pointer will invoke the destructor `~ios_all_saver()`, restoring that state to #m_target_appender_ostream.
324 *
325 * A pointer is used in order to be able to re-construct this at will: `ios_all_saver` does not have a "re-do
326 * construction on existing object" API (not that I'm saying it should... less state is good, all else being equal...
327 * but I digress).
328 *
329 * @see save_formatting_state_and_restore_prev() for explanation of the feature enabled by this member.
330 */
331 boost::movelib::unique_ptr<boost::io::ios_all_saver> m_target_appender_ostream_prev_os_state;
332}; // class Thread_local_string_appender
333
334} // namespace flow::log
Internal flow::log class that facilitates a more efficient way to get util::ostream_op_to_string() be...
void save_formatting_state_and_restore_prev()
Saves the formatting state of the ostream returned by appender_ostream() and sets that same ostream t...
const std::string & target_contents() const
Read-only accessor for the contents of the string, as written to it since the last fresh_appender_ost...
boost::unordered_flat_map< util::Unique_id_holder::id_t, boost::movelib::unique_ptr< Thread_local_string_appender > > Source_obj_to_appender_map
Short-hand for map of a given thread's appender objects indexed by the IDs of their respective source...
std::ostream * appender_ostream()
Same as fresh_appender_ostream() but does not clear the result string, enabling piecemeal writing to ...
util::String_ostream m_target_appender_ostream
The target string wrapped by an ostream. Emptied at construction and in fresh_appender_ostream() only...
static Thread_local_string_appender * this_thread_string_appender(const util::Unique_id_holder &source_obj_id)
Returns a pointer to the exactly one Thread_local_string_appender object that is accessible from the ...
Thread_local_string_appender()
Initializes object with an empty string and the streams machinery available to write to that string.
boost::movelib::unique_ptr< boost::io::ios_all_saver > m_target_appender_ostream_prev_os_state
Stores the ostream formatter state from construction time or time of last save_formatting_state_and_r...
std::ostream * fresh_appender_ostream()
Clears the internally stored string (accessible for reading via target_contents()),...
Similar to ostringstream but allows fast read-only access directly into the std::string being written...
Each object of this class stores (at construction) and returns (on demand) a numeric ID unique from a...
uint64_t id_t
Raw integer type to uniquely identify a thing. 64-bit width should make overflow extremely hard to re...
Flow module providing logging functionality.