Replace legacy strand with executor-templated strand - #10901
Replace legacy strand with executor-templated strand#10901jschmidt-icinga wants to merge 1 commit into
Conversation
bd7aa1a to
ca03f7c
Compare
ca03f7c to
238e6b0
Compare
238e6b0 to
fad5f60
Compare
| ); | ||
| void NewClientHandlerInternal( | ||
| boost::asio::yield_context yc, const Shared<boost::asio::io_context::strand>::Ptr& strand, | ||
| boost::asio::yield_context yc, const std::shared_ptr<IoStrand>& strand, |
There was a problem hiding this comment.
Added a section to the PR description about the reason. In short, it's because on older boost versions boost::asio::spawn() don't know what to do with the Shared<strand>& it gets as a result of dereferencing the pointer.
There was a problem hiding this comment.
In my opinion we should consider deprecating it and just use std::shared_ptr
Absolutely not. I introduced it specifically to save memory. We already had enough memory problems in the past, didn't we?
There was a problem hiding this comment.
What memory does Shared<> save vs. std::shared_ptr<>? 🤔
It will save an additional allocation for the reference count in case someone doesn't use std::make_shared() as one should anyways. And in the rare cases where std::shared_from_this is actually needed, I think it stores an additional std::weak_ptr, but that's negligible for most our classes. At first glance I don't even see anything that would require the latter... we mostly use Shared<> for Stream objects, SSL context and things like that. std::shared_ptr should be perfectly fine for those, or am I missing something.
To be clear: I'm not saying deprecate boost::intrusive_ptr for our Object::Ptr, just shoehorning it into a generic shared pointer, which it was never meant to be.
There was a problem hiding this comment.
Does that even need the external shared pointer? io_context::strand::strand explicitly documented a copy constructor where the copy shares the state, and strand<Executor> has a similar one, but unfortunately without explicit documentation, but from what I've seen in the code, it internally already uses a shared_ptr for its impl_ member (relevant code links: 1, 2, 3), so copies should still refer to the very same strand.
There was a problem hiding this comment.
Right. I remember thinking about that at one point but tried to keep the changes minimal, and then I probably forgot about it again when I replaced Shared<> with std::shared_ptr<>. I'm happy to remove it entirely and just use the copy constructor in these cases.
Can you please share how to come to the same conclusion as you? The documentation for Boost 1.91.0 still mentions them as equal options:
The best I could find was this StackOverflow answer, but that's also more or less just "I looked at the code and came to that conclusion" without further references. I presume that's what you did as well, but can you suggest something to specifically look for in the implementations to come up with the same conclusion? Apart from that, I quickly scrolled over the changes and they look plausible, but that's far from a proper review, but unless someone specifically says I should take a close look at this, I'd that isn't necessary. |
For one it is newer, much more flexible, embraces all the modern concepts, such as executors and has less technical debt (such as the described issue). With the strand-sharing issue being the primary reason, I also wanted to make it clear that we're not falling back to some compatibility version, but to something that is more flexible and modern (in the context of ASIO). Note that I didn't claim |
|
Looking into this turned out to be quite confusing. The history of both implementation files goes back to the beginning of the Git history in 2006 (I didn't bother to go further and try to dig out SVN history). And
However, that doesn't seem the refer to the current implementation of
Finally, that list, which is still present in the current version of the documentation, actually states that
Now, onto the shared implementation between different strand instances: This should be the corresponding part in |
|
Hold on, the confusion got worse again... 🤯 Is there really both |
I was aware of this, and while it looks similar to what This is the most relevant section where those mutexes are used. You can see that nothing blocking happens while it is locked. All it does under the lock is update the state of the waiting and ready queues. |
Yes, but considering that |
What do I need to look at to come up with the same conclusion. The more I try, the more confused I am. If you The problem with this PR is that the description sounds plausible, but doesn't provide any references backing it. So it it's incredibly hard for a reviewer to verify the claims. I mean I've been digging through Asio's documentation, source, and its Git history for quite a bit now and I'm still unsure what would be the preferred strand implementation and why. Apart from this, the PR accumulated some merge conflicts in the meantime. |
I can remove the "old" and "more modern" phrasing from the description if that helps you, but it's mostly irrelevant for the reason I made this PR. The "more modern" part is mostly inferred from the fact that it can work with any executor, which in my mind makes the "old" strand legacy, because what else besides compatibility with old code could be the reason to keep it around? |
fad5f60 to
f1bbf1d
Compare
|
Older and more modern were claims where I was hoping that this would be easy to verify by checking the documentation. As this PR stands right now, I don't see how to do a proper review of it without basically having reviewing both Asio strand implementations. |
I mean technically both strands were added in 1.66, but only the templated one was really new, while the other was renamed from So your issue seems to be more along the lines that there is no clear "use this now" from the boost developers. Which I don't quite get, since there also isn't any "don't use this" for the templated strand. What remains is the concrete issue it solves for us. And if you can't make that decision without auditing the entirety of both implementations, then that's what you'll have to do. I certainly didn't, I just verified that it solves the sharing issue. Or alternatively we can just leave things as they are and hope it doesn't break anything again (perhaps in an even more subtle and hard to debug way). |
This replaces
boost::asio::io_context::strandwith the executor-templatedboost::asio::strand<boost::asio::io_context_executor_type>. This should prevent issues like #10825 to occur in the future, because the modern strand does not share implementation pointers between individual strand objects.Detailed Description
The issue with the previously used strand was that there is a chance for two strand users (like a
PerfdataWriterConnectionand aJsonRpcConnection) to share an implementation object. This means that even though both objects hold distinct strand objects, one can block the execution of the other. So when one objects holds a lock, waits on an IO-Operation on that strand and the other object resumes on the strand and tries to acquire the same lock, both objects are deadlocked.The executor-templated strand type is never shared between users and so should never run into this kind of deadlock. It's available for all boost versions we currently support and I've checked the changelog and git history carefully to see if there are any critical bugs that have been fixed since it was introduced. Technically boost-1.66 was the version this new strand has been introduced, but it seems there have been relatively few changes outside of some refactoring since then.
Currently this is all in one commit, because it's hard to switch this over gradually with some critical pieces (
TimeoutandGracefulDisconnectmost notably) using the strand type explicitly and make incompatible calls on it which can't easily be generalized. And I didn't want to go through the effort of adding an overload either, just so it becomes pointless after switching everything over.Notes
Use of
std::shared_ptr<strand>instead ofShared<strand>::PtrWhen a
Shared<Foo>::Ptris dereferenced, the object is of typeSharedthat inherits fromFoo. When passed toSpawnCoroutine(), this keeps older boost versions from using their dedicatedboost::asio::spawn(const strand< Executor > & ex, ...)overloads and falls back to the generic Executor based overloads, which then fail somewhere in the machinery (the reason the overload exists).Newer boost versions don't have this problem and the strand can just be treated as a generic executor, but for now we'll have to use
std::shared_ptr<strand>for shared strands, because that correctly dereferences to the original type. The alternative would have been explicit object slicing withstatic_cast<strand&>at each call-site.Honestly, I hadn't looked that closely at
Shared<>::Ptr, but this makes it a giant ball of anti-patterns in my eyes. Most egregious of all, unconstrained deriving from arbitrary types isn't a good idea and might break all kinds of things. In my opinion we should consider deprecating it and just usestd::shared_ptrwithstd::make_sharedandstd::shared_from_thisin new code.Closes #10861.