Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
thread_cache.cpp
Go to the documentation of this file.
1/* Flow-IPC: SHM-jemalloc
2 * Copyright (c) 2023 Akamai Technologies, Inc.; and other contributors.
3 * Each commit is copyright by its respective author or author's employer.
4 *
5 * Licensed under the MIT License:
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE. */
24
25/// @file
28#include "ipc/common.hpp"
29#include <flow/util/util.hpp>
30#include <jemalloc/jemalloc.h>
31#include <vector>
32
34{
35
36// Static initializers.
37
38Thread_cache::Static_state Thread_cache::s_state;
39
40// Implementations.
41
43{
44 const auto cache = s_state.m_caches.this_thread_state_or_null();
45 if (!cache)
46 {
47 return s_state.m_caches.this_thread_state(); // Create and return it.
48 }
49 /* else: Before returning existing cache, we may want to destroy some tcaches in it (see destroy_arena_safely()).
50 * (Do note 99.99999% of the time there is no such work pending, and the following call returns after a couple
51 * of instructions, a no-op. Meanwhile this call is fully opportunistic piggy-backing.) */
52
53 cache->if_requested_destroy_tcaches_and_possibly_finish_arena_kills();
54
55 return cache;
56}
57
59{
60 const auto cache = s_state.m_caches.this_thread_state_or_null();
61 if (!cache)
62 {
63 return nullptr; // Nota bene.
64 }
65 // else
66 cache->if_requested_destroy_tcaches_and_possibly_finish_arena_kills();
67 return cache;
68}
69
70Thread_cache::Thread_cache(flow::log::Logger* logger_ptr) :
71 flow::log::Log_context_mt(logger_ptr, Log_component::S_SHM),
72 m_thread_token(flow::util::this_thread_unique_token())
73{
74 namespace this_thread = flow::util::this_thread;
75 using flow::util::Thread_id;
76 using flow::log::Logger;
77
78 Thread_id ignored;
79 Logger::set_thread_info(&m_thread_nickname, &ignored);
80
81 // That's it for now; id() -- likely to be called very soon -- will create an actual tcache.
82
83 // All good; ostream<< will print all the stuff (thread token and nickname, etc.) incidentally.
84 FLOW_LOG_INFO_LOCKED("Jem_tcache[" << *this << "]: "
85 "Created cache object for this thread (details to the left in this message); "
86 "no tcaches yet until next id() call.");
87} // Thread_cache::Thread_cache()
88
90{
91 const auto this_thread_unique_token = flow::util::this_thread_unique_token();
92 if (this_thread_unique_token != thread_token())
93 {
94 FLOW_LOG_WARNING_LOCKED
95 ("Jem_tcache[" << *this << "]: "
96 "Shutting down from different thread (unique-token [" << this_thread_unique_token << "]) "
97 "than the one (unique-token [" << thread_token() << "]) that created us; "
98 "cannot flush/destroy anything; bailing out. In any case, this can conceivably -- in the "
99 "absence of misuse by the Thread_cache user -- only occur when program is exiting, at which "
100 "point graceful shutdown of jemalloc resources is arguably less essential.");
101 return;
102 }
103 // else
104
105 /* Sanity-check about Polled_shared_state<> s_state.m_arenas_death_row_map: Recall it maps arena ID => set of
106 * `this`es like us which should do if_requested_destroy_tcaches_and_possibly_finish_arena_kills().
107 * Now, there's a s_state.m_caches.while_locked() section in destroy_arena_safely() that loads `this` into that map
108 * (and then arms thread-local flag to be checked by if_requested_destroy_tcaches_and_possibly_finish_arena_kills()).
109 * Also though this thread is exiting, and Thread_local_state_registry<Thread_cache> in its cleanup function
110 * also has s_state.m_caches.while_locked() which removed `this` from the registry of extant `Thread_cache`s; this
111 * has already happened -- which is why we're being deleted -- and we are outside that while_locked(): both
112 * formally guaranteed (see the lifecycle section in Thread_local_state_registry doc header). So: either
113 * we've been added into m_arenas_death_row_map before that happened, or we couldn't
114 * be added into it anymore, as we weren't extant during the first s_state.m_caches.while_locked() mentioned above.
115 * The important thing is that at *this* point `this` will never be added into s_state.m_arenas_death_row_map again;
116 * so like if we were to remove it from there in this dtor, it's not concurrent against more of that stuff;
117 * we're "safe" from that craziness now. However! What if we're in m_arenas_death_row_map right now? It's then
118 * quite important to remove us! If we don't do it now, it can never happen, and the damned arena(s) will never
119 * get destroyed. No problem: while `this` exists (until this dtor finishes) we can still do it.
120 * So we in fact do so below: if_requested_destroy_tcaches_and_possibly_finish_arena_kills().
121 * We don't bother checking the flag but simply assume it's armed this one time: thread exit is infrequent,
122 * and the scan is cheap when nothing is pending. */
123
124 FLOW_LOG_INFO_LOCKED("Jem_tcache[" << *this << "]: "
125 "Shutting down. First, will check for any pending arena destruction progress to push-forward; "
126 "this will destroy 0+ tcaches, destroy 0+ arenas, run 0+ on-done functors for the latter; next "
127 "will destroy (includes flushing) any remaining tcaches.");
128
129 // Tell it not to check the flag and just act as-if it's armed (see above).
131
132 for (const auto& arena_and_tcache_id : m_tcaches_by_arena)
133 {
134 destroy_tcache(arena_and_tcache_id.second);
135 }
136} // Thread_cache::~Thread_cache()
137
139{
140 using flow::util::this_thread_unique_token;
141 using boost::system::system_category;
142
143 assert((this_thread_unique_token() == thread_token()) && "Please don't flush/etc. across threads.");
144
145 FLOW_LOG_INFO_LOCKED("Jem_tcache[" << *this << "]: Tcache-flush of tcache [" << id << "] requested: proceeding.");
146
147 const auto code = IPC_SHM_ARENA_LEND_JEMALLOC_API(mallctl)("tcache.flush", nullptr, nullptr, &id, sizeof(id));
148 if (code != 0)
149 {
150 const Error_code sys_err_code{code, system_category()};
151 FLOW_LOG_FATAL_LOCKED("System error occurred: [" << sys_err_code << "] [" << sys_err_code.message() << "].");
152 assert(false && "Tcache-flush failed -- as of this writing we consider this catastrophic; "
153 "for some ops such as to allow for arena destruction subsequently it is essential "
154 "for it to succeed. We don't consider this recoverable. @todo Revisit.");
155 std::abort();
156 }
157 // else
158} // Thread_cache::flush_tcache()
159
161{ // @todo Code reuse versus flush_tcache()?
162 using flow::util::this_thread_unique_token;
163 using boost::system::system_category;
164
165 assert((this_thread_unique_token() == thread_token()) && "Please don't flush/etc. across threads.");
166
167 FLOW_LOG_INFO_LOCKED("Jem_tcache[" << *this << "]: Tcache-destruction (includes flushing) of tcache [" << id << "] "
168 "requested: proceeding.");
169
170 const auto code = IPC_SHM_ARENA_LEND_JEMALLOC_API(mallctl)("tcache.destroy", nullptr, nullptr, &id, sizeof(id));
171 if (code != 0)
172 {
173 const Error_code sys_err_code{code, system_category()};
174 FLOW_LOG_FATAL_LOCKED("System error occurred: [" << sys_err_code << "] [" << sys_err_code.message() << "].");
175 assert(false && "Tcache-destruction failed -- as of this writing we consider this catastrophic; "
176 "for some ops such as to allow for arena destruction subsequently it is essential "
177 "for flushing and/or destruction to succeed. We don't consider this recoverable. "
178 "@todo Revisit.");
179 // Technically only flushing is required; but let's just keep it consistent: this failing is too weird.
180 std::abort();
181 }
182 // else
183} // Thread_cache::destroy_tcache()
184
186{
187 const auto it = m_tcaches_by_arena.find(arena_id);
188 return (it == m_tcaches_by_arena.end()) ? S_NO_TCACHE_ID : it->second;
189}
190
192{
193 using boost::system::system_category;
194
195 constexpr tcache_id_t DUMMY_ID = S_NO_TCACHE_ID;
196
197 const auto it_and_did_insert = m_tcaches_by_arena.emplace(arena_id, DUMMY_ID);
198 auto& id = it_and_did_insert.first->second; // This is the tcache_id_t for arena_id.
199
200 if (it_and_did_insert.second) // I.e.: if (it was not already there; we inserted it)
201 {
202 const auto mallctl_or_die = [&](const char* op_name, void* oldp, size_t* oldlenp, void* newp, size_t newlen)
203 {
204 const auto code = IPC_SHM_ARENA_LEND_JEMALLOC_API(mallctl)(op_name, oldp, oldlenp, newp, newlen);
205 if (code != 0)
206 {
207 const Error_code sys_err_code{code, system_category()};
208 FLOW_LOG_FATAL_LOCKED("Op [" << op_name << "]: System error occurred: [" << sys_err_code << "] "
209 "[" << sys_err_code.message() << "].");
210 assert(false && "Tcache-creation-related mallctl() failed -- as of this writing we consider this "
211 "catastrophic. "
212 "@todo We could probably add contingency steps and deal with it; just for the time "
213 "being we've seen no legit reason for this to fail and decided not to spend effort "
214 "in coding and testing such contingencies.");
215 std::abort();
216 }
217 }; // auto mallctl_or_die =
218
219 /* `id` is the dummy-value; we must create tcache and place its actual ID there.
220 *
221 * (Note: jemalloc auto-associates the new tcache -- an association governing stats-filing only -- with an
222 * arena chosen via a thread-internal binding that public jemalloc API cannot influence; in particular it
223 * is *not* `arena_id`, and bracketing the creation with `thread.arena` writes does not change it (verified
224 * in 5.3.0 source: the choice is arena_ichoose(), the *internal*-arena chooser, which `thread.arena` does
225 * not touch). Consequence for our stats documented in Memory_manager_stats::Alloc doc header.) */
226 size_t output_sz = sizeof(id);
227 mallctl_or_die("tcache.create", &id, &output_sz, nullptr, 0);
228
229 FLOW_LOG_INFO_LOCKED("Jem_tcache[" << *this << "]: A tcache ID was requested for an arena but did not exist; thus: "
230 "Tcache-create succeeded; tcache [" << id << "] for arena [" << arena_id << "] ready.");
231 } // if (arena_id was not in m_tcaches_by_arena)
232 // else { Fast-path. }
233
234 return id;
235} // Thread_cache::id()
236
238{
239 using std::vector;
240
241 // Avoid logging work, or any unneeded work of any kind, in the fast-path....
242
243 if ((!assume_requested) && (!m_destroy_tcaches_requested_poll_flag.poll_armed()))
244 {
245 return;
246 }
247 /* else if (it was requested) || assume_requested: We will do it.
248 * Unless assume_requested: It is now re-marked as not-requested. */
249
250 // Non-fast-path is in effect (can log, etc.).
251
252 vector<decltype(Arena_death_progress::m_on_done_func)> on_done_funcs_to_execute;
253 s_state.m_arenas_death_row_map.while_locked([&](auto* arenas_death_row_map_ptr)
254 {
255 auto& arenas_death_row_map = *arenas_death_row_map_ptr;
256 vector<arena_id_t> arenas_to_kill;
257
258 for (auto& arena_and_progress : arenas_death_row_map)
259 {
260 auto& arena_death_progress = arena_and_progress.second;
261 auto& caches = arena_death_progress.m_caches_left_to_destroy;
262 if (caches.erase(this) == 0)
263 {
264 continue; // This arena's doom is (no longer?) dependent on our having destroyed a tcache of ours.
265 }
266 // else
267
268 const bool fatal_destroy = caches.empty();
269 const auto arena_id = arena_and_progress.first;
270 const auto it = m_tcaches_by_arena.find(arena_id);
271 if (it == m_tcaches_by_arena.end())
272 {
273 FLOW_LOG_INFO_LOCKED("Jem_tcache[" << *this << "]: Arena [" << arena_id << "] is being destroyed; "
274 "this thread-cache object was marked to destroy any relevant tcache before "
275 "arena-destruction can proceed; however that arena has not been used w/r/t tcaching "
276 "in this thread yet; therefore there is nothing to destroy, and we shall cross ourselves "
277 "off that list. If that makes the list empty (does it? = [" << fatal_destroy << "]), "
278 "we shall imminently destroy arena.");
279 }
280 else
281 {
282 const auto id = it->second;
283 FLOW_LOG_INFO_LOCKED("Jem_tcache[" << *this << "]: Arena [" << arena_id << "] is being destroyed; "
284 "this thread-cache object was marked to destroy any relevant tcache before "
285 "arena-destruction can proceed; and indeed we hold tcache [" << id << "]. "
286 "Proceeding with destruction to cross ourselves off the list. If that makes "
287 "the list empty (does it? = [" << fatal_destroy << "]), we shall imminently "
288 "destroy arena.");
289 destroy_tcache(id);
290 m_tcaches_by_arena.erase(it);
291 }
292
293 if (fatal_destroy)
294 {
295 arenas_to_kill.push_back(arena_id);
296
297 on_done_funcs_to_execute.emplace_back(std::move(arena_death_progress.m_on_done_func));
298 arena_death_progress.m_on_done_func.clear(); // Just in case move() didn't do it.
299 }
300 } // (arena_and_progress : arenas_death_row_map)
301
302 // Clean out the entries we made empty (for which we'll run on_done_func() just below, finishing the exercise).
303 for (const auto arena_id : arenas_to_kill)
304 {
305 const auto it = arenas_death_row_map.find(arena_id);
306#ifndef NDEBUG
307 auto& arena_death_progress = it->second;
308#endif
309 assert(arena_death_progress.m_caches_left_to_destroy.empty());
310 assert(arena_death_progress.m_on_done_func.empty());
311
312 arenas_death_row_map.erase(it);
313 }
314 }); // s_state.m_arenas_death_row_map.while_locked()
315
316 for (auto& on_done_func : on_done_funcs_to_execute)
317 {
318 on_done_func(static_cast<const Log_context_mt*>(this));
319 }
320} // Thread_cache::if_requested_destroy_tcaches_and_possibly_finish_arena_kills()
321
322void Thread_cache::caches_set_logger(flow::log::Logger* logger_ptr) // Static.
323{
324 s_state.m_caches.set_logger(logger_ptr); // It'll propagate it to extant and future per-thread objects.
325}
326
327void Thread_cache::destroy_arena(arena_id_t arena_id, const flow::log::Log_context_mt* log_ctx) // Static.
328{
329 using flow::util::ostream_op_string;
330 using flow::util::Lock_guard;
331 using flow::util::Mutex_non_recursive;
332 using boost::system::system_category;
333
334 FLOW_LOG_SET_LOCKED_CONTEXT(log_ctx);
335
336 size_t input_sz = 0;
337 int code;
338 {
339 /* arena.*.destroy() mutates the jemalloc arena set, so serialize it against any concurrent
340 * stat-dumps, arena-create (and other arena-destroys). See detail::jemalloc_arena_list_mutex() doc header
341 * for the whole story. Do note that if some other code does arenas.create et al directly, then this
342 * won't beat it; but if they use our nice APIs then all good. */
343 Lock_guard<Mutex_non_recursive> lock{detail::jemalloc_arena_list_mutex()};
344
345 code = IPC_SHM_ARENA_LEND_JEMALLOC_API(mallctl)(ostream_op_string("arena.", arena_id, ".destroy").c_str(),
346 nullptr, nullptr, nullptr, input_sz);
347 }
348
349 if (code != 0)
350 {
351 /* Same as other jemalloc failures in this class. Throwing is not really an option anyway: we may be (and are,
352 * when deferred) invoked from this_thread_cache*() -- called at times even from noexcept contexts such as
353 * Ipc_arena::deallocate() -- or from ~Thread_cache().
354 *
355 * Incidentally: in practice we only know of this occurring, if arena_id is invalid. */
356 const Error_code sys_err_code{code, system_category()};
357 FLOW_LOG_FATAL_LOCKED("Arena [" << arena_id << "] destruction: System error occurred: [" << sys_err_code << "] "
358 "[" << sys_err_code.message() << "].");
359 assert(false && "Arena-destruction failed -- as of this writing we consider this catastrophic. @todo Revisit.");
360 std::abort();
361 }
362 // else
363 FLOW_LOG_INFO_LOCKED("Destroyed arena [" << arena_id << "].");
364} // Thread_cache::destroy_arena()
365
366flow::util::Thread_token Thread_cache::thread_token() const { return m_thread_token; }
367const std::string& Thread_cache::thread_nickname() const { return m_thread_nickname; }
368
370 m_caches(nullptr, // Logger*. Can `Thread_cache::caches_set_logger()`, e.g., in main().
371 "JemThreadCaches")
372{
373 Set_logger_registry::register_action([](flow::log::Logger* logger_ptr)
374 {
375 caches_set_logger(logger_ptr);
376 });
377 // Now arena_lend::set_logger(x) will, among others potentially, do our `caches_set_logger(x)`.
378}
379
380std::ostream& operator<<(std::ostream& os, const Thread_cache& val)
381{
382 /* Print only immutable stuff: e.g., destroy_arena_safely() prints other threads' `*this`es.
383 * (So, e.g., no m_tcaches_by_arena.size().) */
384 os << "[thread=[tok-" << val.thread_token();
385 if (!val.thread_nickname().empty())
386 {
387 os << '|' << val.thread_nickname();
388 }
389 return os << "]]@" << &val;
390}
391
392} // namespace ipc::shm::arena_lend::jemalloc
jemalloc central thread cache manager, maintaining an explicitly identified tcache per allocating thr...
flow::util::Thread_token thread_token() const
Unique-token of thread in which *this was originally created via this_thread_cache() call.
void if_requested_destroy_tcaches_and_possibly_finish_arena_kills(bool assume_requested=false)
Perform deferred cross-thread tcache and arena-destruction as triggered by destroy_arena_safely() cal...
void flush_tcache(tcache_id_t id)
Flushes the given tcache as specified by its ID.
friend std::ostream & operator<<(std::ostream &, const Thread_cache &)
Prints string representation of the given Thread_cache to the given ostream.
static Static_state s_state
See doc header and especially member doc headers for Static_state.
flow::util::Thread_token m_thread_token
See thread_token().
std::string m_thread_nickname
See thread_nickname().
boost::unordered_flat_map< arena_id_t, tcache_id_t > m_tcaches_by_arena
See id(): for this thread, maps arena ID (that has been passed to id() at least once) to the jemalloc...
static constexpr tcache_id_t S_NO_TCACHE_ID
Special value returned by id_or_none() indicating no tcache yet exists for the given arena_id_t.
~Thread_cache()
Destructor that runs in each thread (potentially even after main() – the thread had not been joined b...
Thread_cache(flow::log::Logger *logger_ptr)
Constructs cache for this thread as invoked internally by this_thread_cache().
static Thread_cache * this_thread_cache_or_null()
Returns pointer to the one cache-object in the calling thread, or null if none exists (this_thread_ca...
static Thread_cache * this_thread_cache()
Returns pointer to the one cache-object in the calling thread, first creating it if it is the first s...
flow::util::Poll_flag m_destroy_tcaches_requested_poll_flag
Poll-flag corresponding to Static_state::m_arenas_death_row_map Polled_shared_state pattern.
tcache_id_t id(arena_id_t arena_id)
Returns tcache ID of the tcache in this thread with a 1-1 relationship to the given arena,...
const std::string & thread_nickname() const
Thread nickname, as from flow::log::Logger::set_thread_info(), corresponding to thread in which *this...
void destroy_tcache(tcache_id_t id)
Flushes/destroys the given tcache.
tcache_id_t id_or_none(arena_id_t arena_id) const
Returns tcache ID of the tcache in this thread with a 1-1 relationship to the given arena,...
static void destroy_arena(arena_id_t arena_id, const flow::log::Log_context_mt *log_ctx)
Destroys the given arena now.
static void caches_set_logger(flow::log::Logger *logger_ptr)
Sets the Logger to use (or null to skip logging) subsequently for logging in static calls; non-static...
flow::util::Mutex_non_recursive & jemalloc_arena_list_mutex()
Returns the process-global mutex serializing any mutation of jemalloc's arena set (arena create/destr...
ipc::shm sub-module with the SHM-jemalloc SHM-provider. See ipc::shm doc header for introduction.
unsigned int arena_id_t
Arena ID as dictated by jemalloc.
unsigned int tcache_id_t
Thread cache ID type as dictated by jemalloc.
Log_component
The flow::log::Component payload enumeration containing various log components used by Flow-IPC inter...
Definition: common.hpp:338
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313
#define IPC_SHM_ARENA_LEND_JEMALLOC_API(ARG_api_name)
To be used when invoking jemalloc public API functions, this evaluates to the given argument (which s...
Definition: jemalloc.hpp:87
Function< void(const flow::log::Log_context_mt *)> m_on_done_func
Functor to invoke, and then immediately destroy, once m_caches_left_to_destroy is ....
flow::util::Polled_shared_state< boost::unordered_map< arena_id_t, Arena_death_progress > > m_arenas_death_row_map
State shared – and acted upon using the Polled_shared_state pattern – between extant Thread_cache per...
flow::util::Thread_local_state_registry< Thread_cache > m_caches
The work-horse: registry of per-thread Thread_cache objects, each holding 0+ jemalloc tcaches.