-
Notifications
You must be signed in to change notification settings - Fork 4.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Is there a way to cleanly use spdlog after main() (i.e. in static / global objects' destructors)? #1738
Comments
Actually it's even more subtle than that, the Singletons are in an Windows .DLL, and their destructors run after |
As far as I know, after the execution of It is also difficult to control the destructor call order of the static object: https://stackoverflow.com/questions/469597/destruction-order-of-static-objects-in-c |
The call order looks fine, but I did figure out what the problem was. After The long and short of it seems to be that I just shouldn't use spdlog past |
Yes, thread does not work in |
In any case, I'm happy with my findings, please feel free to close the issue anytime you want. |
The workaround is to prepare a startup function and a cleanup function (like Winsock's |
Yep, that's the route I'm going for. |
Actually, just one more thing: to be OK do I call just |
Calling it is only OK with |
@tt4g is there an open issue already regarding the destructor limitations in spdlog? I know that, with enough visiting volunteer effort, pretty much any problem can be eventually solved. It helps a lot of there is a clear way to reproduce the issue and a location to share information on it. Are you familiar with the construct on first use idiom and the nifty/schwartz counter idiom? If not, they are often used to address problems like these, but it sounds like the issue might relate to specifics of dll loading as well. |
@xloem Calling in destructors is not the essence of the problem. The problem is simple: I am aware that this is not a spdlog problem, but rather a problem caused by the automatic cleanup process in Windows OS that does not identify released resources. On Windows platforms, it is better to call |
Do you have a link to the windows cleanup issue somewhere? Are you saying that the static object is destroyed before everyone is done using it, or that the thread is being closed by windows before the static object is destroyed, or that a dynamic library is being unloaded before everyone is done using it, or something else? Is there a way I or a visitor could reproduce a failure here? I'm just thinking that if we slowly collected information in an open issue, people could eventually come up with an organised workaround you'd appreciate. There's always some workaround. |
I believe this is not a problem, but a specification (or limitation).
The startup process should load the application function symbols, etc. into memory, allocate the stack into memory, etc. The cleanup process then releases resources such as dynamic memory, threads, and mutexes that have not been released by the time the application terminates. This startup and cleanup behavior is not directly related to the C++ specification. However, it is due to the C++ specification that https://en.cppreference.com/w/cpp/language/destructor
The The problem arises because the two different operating specifications do not (cannot) take each other into consideration. To add to this, it seems that different versions of the Windows OS have different start-up and clean-up behaviors. |
Yes, you can reproduce it. (In a DLL) just create a global object that logs something on the destructor, and set the default spdlog logger to an async logger. If you then run your application enough times, it'll either hang or crash. FWIW, last time I did this the EXE and DLL were using a shared runtime, with Visual Studio 2019. The concise explanation for this is that by the time the destructor of the global DLL object gets called, Windows has killed all of the other threads. You will only have one thread running. But that thread is trying to log something, by placing the thing to be logged in spdlog's queue and signaling it (which involves mutexes, condition variables, something of that sort). Another thread was supposed to pick up the thing posted in the queue and output it to its final destination. But that thread is dead (remember, they've all been killed by Windows), possibly just after it locked the mutex you're trying to use to post to the queue (in which case you're deadlocked, hence the hang). More info: https://docs.microsoft.com/en-us/windows/win32/dlls/dllmain (see the Warning sections) I would also politely disagree with the statement "with enough visiting volunteer effort, pretty much any problem can be eventually solved". I applaud the sentiment, but I dispute its accuracy. :) |
Thanks for your description of how to reproduce, that's a great path for pursuing this further. I have read some of your references. It sounds like this particular issue is present within the dll that runs the log thread, but not in dlls it loads, and only on windows. It sounds like the situation could also be detected with a flag to make it safe (but nonfunctional) to log from such destructors.
Well, I am thinking on a scale of many years, when I say that, but of course it usually does not happen, it just can. This WSL issue was opened in 2016; I shared a workaround in 2019 on that thread that functioned by actually hooking the windows system calls and changing their behavior, and people actually used this. Microsoft then resolved the underlying issue in their next release. |
Yes, it's a Windows-only issue. See also: https://spdlog.docsforge.com/v1.x/0.faq/#asynchronous-logging-hangs-on-application-exit-under-windows In fact, the application where the problem mainfested was cross-platform (Linux, macOS and Windows) and exhaustively tested on all platforms. Only Windows had any spdlog problems. |
The problem when calling spdlog::shutdown from dllmain PROCESS_DETACH event is caused by code inside this function that tries to send notification to all threads created by spdlog to close and starts waiting for these threads to exit. But the PROCESS_DETACH event is called by Windows after all threads but the last one are already closed by OS. For me it was enough to disable periodic flush before exiting WinMain (and enable flush on every message level instead). In this case no threads created by spdlog are alive at the moment of dllmain calling spdlog::shutdown I do not know if there is an API that could be used to close only async loggers and cause the threads related to them to be stopped before exiting WinMain and leave sync loggers without periodic flush to live till the process end |
@gabime @xloem My question is, can we introduce something along the lines that @xloem mentioned, or atleast some sort of capability of letting registry know that there are still some handles out there, etc... Also, I think in our case it would be best to maybe go ahead with the above proposed nifty counter, if we create a logger manually. Would that work, eg. does the life of manually created logger extend over the registry (and potentially other spdlog internals) destruction? A minimal code that represents the issue (tested /w compiled spdlog 1.9.2, app compiled with flags:
|
I suppose if you will use spdlog in the constructor of the global object it will force to construct registry before your global object in this case it will be still alive to be used in destructor as well And as a hint - do not use globals. Use static vars in the GetInstance() function - to avoid issues when one global var tries to use another one. When you call GetInstance() function it is guaranteed that any static var in it is initialized. When you access global var from other global var - there is no such guarantee |
@artemyv the issue at hand is calling spdlog from destructor of a global object. |
The order of destruction is opposite to order of construction. You need to ensure the right order of construction to be able to use logger in destructors. That's why my suggestion was about usage of logger in constructor. |
I had gotten confused by the addition of a different concern to the discussion, but I'm popping back here just a little bit. To clarify, it looks like @themarpe's case would be resolved via a nifty/schwartz counter or shared pointer on the structure (maybe the registry, I infer) containing the data that is deallocated too early. I tried the example code with latest spdlog commit, and I also tried changing it to call the logger in the constructor as artemyv suggested: both failed for me. (edit: this could be because the registry is allocated as a static local variable) This problem is distinct from the DLL unloading concern which is likely a little more complex. I infer that #2113 is tracking @themarpe's platform-independent deallocation order issue, and that there is still no issue tracking the windows dll unloading issue. |
Godbolt of the above bug: https://godbolt.org/z/9qxo4Meex @xloem / @wangjunZeroZeroSeven - rereading the #2113 and above, what do you think about doing such change to spdlog:
Where
Note, not tested yet, just an idea |
I'm still just casually looking at this; I have not understood the problem in depth. It sounds to me like the simplest solution would be to bump a major version and change the api to have c-style initialisation and destruction as recommended at #2113 (comment) . This would simplistically resolve nearly all the potential problems here, but would be a major change to the library. There's a good chance the maintainers would not accept such a change. Getting feedback from them via tagging or opening a pr could move that decision forward. Second to that the solution would of course be to wrap everything in one object and provide access to that object as you recommend, and then document and later implement workarounds for any quirks that exist. If this can be done without breaking the interface then there's a good chance a PR would be accepted. Given people are saying the problems lie with static data that is outside the registry, a first step could be to simply move all the static data into it, as also mentioned in the other issue. This would make progress on both approaches. |
Why has this been closed? I am still facing this issue, has a solution already been proposed and added? |
No, it is considered an invalid usage/limitation of spdlog. You are welcome to propose a solution. |
For static objects, the order of destruction is in reverse of the order of construction, but the latter is hard to control except among static objects defined within the same translation unit. This means that spdlog may be destructed before spdlog::drop() is invoked in the destructor, which leads to a segmentation fault due to memory use-after-free. The spdlog example points out that dropping all loggers via spdlog::shutdown(), which invokes spdlog::drop_all(), is optional. Omit destructor calls of spdlog::drop(), but retain non-destructor calls of spdlog::drop_all() for now since they do not cause any harm. Link: ros2/rclcpp#933 Link: gabime/spdlog#1738 Link: https://github.com/gabime/spdlog/blob/a2b4262090fd3f005c2315dcb5be2f0f1774a005/example/example.cpp#L96 Closes: https://hsdes.intel.com/appstore/article/#/22019839238 Signed-off-by: Peter Colberg <peter.colberg@intel.com>
For static objects, the order of destruction is in reverse of the order of construction, but the latter is hard to control except among static objects defined within the same translation unit. This means that spdlog may be destructed before spdlog::drop() is invoked in the destructor, which leads to a segmentation fault due to memory use-after-free. The spdlog example points out that dropping all loggers via spdlog::shutdown(), which invokes spdlog::drop_all(), is optional. Omit destructor calls of spdlog::drop(), but retain non-destructor calls of spdlog::drop_all() for now since they do not cause any harm. Link: ros2/rclcpp#933 Link: gabime/spdlog#1738 Link: https://github.com/gabime/spdlog/blob/a2b4262090fd3f005c2315dcb5be2f0f1774a005/example/example.cpp#L96 Closes: https://hsdes.intel.com/appstore/article/#/22019839238 Signed-off-by: Peter Colberg <peter.colberg@intel.com>
For static objects, the order of destruction is in reverse of the order of construction, but the latter is hard to control except among static objects defined within the same translation unit. This means that spdlog may be destructed before spdlog::drop() is invoked in the destructor, which leads to a segmentation fault due to memory use-after-free. Move call of spdlog::drop() from destructor to main() function, to ensure the logger may be re-registered with the same name in tests. Link: ros2/rclcpp#933 Link: gabime/spdlog#1738 Closes: https://hsdes.intel.com/appstore/article/#/22019839238 Signed-off-by: Peter Colberg <peter.colberg@intel.com>
For static objects, the order of destruction is in reverse of the order of construction, but the latter is hard to control except among static objects defined within the same translation unit. This means that spdlog may be destructed before spdlog::drop() is invoked in the destructor, which leads to a segmentation fault due to memory use-after-free. Move call of spdlog::drop() from destructor to main() function, to ensure the logger may be re-registered with the same name in tests. Use Scope Guard pattern to implement RAII for logger registration. Link: ros2/rclcpp#933 Link: gabime/spdlog#1738 Link: gabime/spdlog#2113 Link: https://en.wikibooks.org/wiki/More_C++_Idioms/Scope_Guard Closes: https://hsdes.intel.com/appstore/article/#/22019839238 Signed-off-by: Peter Colberg <peter.colberg@intel.com>
For static objects, the order of destruction is in reverse of the order of construction, but the latter is hard to control except among static objects defined within the same translation unit. This means that spdlog may be destructed before spdlog::drop() is invoked in the destructor, which leads to a segmentation fault due to memory use-after-free. Move call of spdlog::drop() from destructor to main() function, to ensure the logger may be re-registered with the same name in tests. Use Scope Guard pattern to implement RAII for logger registration. Link: ros2/rclcpp#933 Link: gabime/spdlog#1738 Link: gabime/spdlog#2113 Link: https://en.wikibooks.org/wiki/More_C++_Idioms/Scope_Guard Closes: https://hsdes.intel.com/appstore/article/#/22019839238 Signed-off-by: Peter Colberg <peter.colberg@intel.com>
For static objects, the order of destruction is in reverse of the order of construction, but the latter is hard to control except among static objects defined within the same translation unit. This means that spdlog may be destructed before spdlog::drop() is invoked in the destructor, which leads to a segmentation fault due to memory use-after-free. Move call of spdlog::drop() from destructor to main() function, to ensure the logger may be re-registered with the same name in tests. Use Scope Guard pattern to implement RAII for logger registration. Link: ros2/rclcpp#933 Link: gabime/spdlog#1738 Link: gabime/spdlog#2113 Link: https://en.wikibooks.org/wiki/More_C++_Idioms/Scope_Guard Closes: https://hsdes.intel.com/appstore/article/#/22019839238 Signed-off-by: Peter Colberg <peter.colberg@intel.com>
Hello (and thanks for your work on spdlog),
Is there an endorsed way of logging from global / static objects' destructors (that is, after
main()
is done)?Specifically, I want to use a rotating, async logger, and have a static Singleton that's supposed to manage spdlog. The static object's destructor will do a
drop_all()
and no other objects are supposed to be using spdlog after that.I know that the recommended way is to call
drop_all()
(orshutdown()
? I've seen both being recommended, and not together - should I just use one of them? both? if both, I assumedrop_all()
beforeshutdown()
?).Anyway, I'm seeing what appears to me to be the right order of construction and destruction, that is
~thread_pool()
and~registry
run after my Singletons' destructors, but I am losing a bunch of messages (pretty much all that's logged aftermain()
doesn't show up - I've triedsleep()
s beforedrop_all()
with no success). That doesn't occur when I am using the default stdout spdlog logger.Long story short, is there a recommended way to be able to go about achieving this, or is logging after
main()
just not done under any circumstances? And for debugging, what other spdlog globals should I be watching out for (are there any others, other thanregistry
andthread_pool
)?The text was updated successfully, but these errors were encountered: