Open timmaxw opened 9 years ago
I like the "We could make auto_drainer_t have a non-trivial constructor" option because it's not much work (especially the remember_to_consider_destructor_order_t
variant) and would likely be a noticeable improvement already.
The explicit stop()
seems slightly more effective. I'm a little bit concerned that it might become annoying to use though.
Static analysis seems to give the strongest guarantees. We could even consider enforcing that all class members come before an auto drainer, unless they're somehow added to a white list. This solution seems to be very work-intensive though, and I'm not convinced that this would be work well spent.
I don't think the remember_to_consider_destructor_order_t
buys us much; auto_drainer_t
already tells me that (although we could make the name scarier I suppose).
I asked this before, I think, but I forgot the answer: why can't we just make the constructors for the subscription-like classes take a reference to an auto drainer (which they promptly ignore)? That way you'd know that an auto drainer had already been constructed when you construct them, which is reasonably strong evidence that the auto drainer will be destroyed second.
The advantage of remember_to_consider_destructor_order_t
is that it gives the auto_drainer_t
a non-trivial constructor, so it must appear in the class's initializer list. The idea is that this would make people think a bit harder. Also, this proposal is mostly for the benefit of new engineers and outside contributors who might not automatically think about destructor order when they see auto_drainer_t
.
The problem with making subscription-like classes take a reference to an auto_drainer_t
is that it often makes sense to use a subscription-like class without an auto drainer. You only need an auto_drainer_t
if you're planning to spawn coroutines from your subscription callback.
What if we made the subscription-like class take a pointer to an auto-drainer, or NULL if it doesn't need an auto drainer, and if the subscription-like class receives NULL then it does ASSERT_NO_CORO_WAITING
before calling its callback to ensure that it actually didn't need the auto drainer? (Or a boost optional if you don't want people passing NULL pointers out like candy.)
Most subscription-like classes' callbacks already do ASSERT_NO_CORO_WAITING
. The problem isn't that the callback will block; it's that the callback will spawn a coroutine.
Er, right. Could we add something similar to ASSERT_NO_CORO_WAITING
which errors on spawn_sometime
as well as spawn_now_dangerously
?
That would produce a lot of false positives, because it's possible for a callback to unknowingly spawn a coroutine on the other side of an abstraction boundary. For example, it's reasonable for a callback to modify the value of a watchable_variable_t
, since modifying the value of a watchable_variable_t
is guaranteed not to block. But maybe a completely different object is watching that watchable_variable_t
, and in response to the change, it spawns a new coroutine governed by its own auto_drainer_t
.
In our code, we have many classes which will automatically call a callback at any time until they are destroyed. Often these classes are used as members of other classes, and the callback accesses other members of the class. Sometimes the callback spawns coroutines which access other members of the class. The safe way to put this together is as follows:
When the destructor is run, first
subs
is destroyed, which prevents calls tocallback1()
. Thendrainer
is destroyed, which blocks until all instances ofcallback2()
have finished. This ensures thatcallback1()
andcallback2()
cannot accessfoo
andbar
when they are being destroyed.The problem is that people often put this class together in the wrong order. Sometimes they forget the
auto_drainer_t
(see #3552). Other times they putdrainer
aftersubs
, which will lead to crashes ifcallback1()
runs during or after thedrainer
destructor. Or they may putfoo
andbar
afterdrainer
or aftersubs
, which means that their destructors may be called whilecallback1()
andcallback2()
can still access them. This is a source of subtle bugs.The following solutions have been proposed:
auto_drainer.hpp
, and be sure to educate new engineers about these issues. But documentation is fragile and nobody reads it.auto_drainer_t
have a non-trivial constructor, to encourage people to think about constructor order. One option is thatauto_drainer_t
should take an initializer-list ofvoid*
, so it would be initialized asdrainer(&foo, &bar)
; this would symbolically indicate thatfoo
andbar
must be constructed beforedrainer
. Another option is to make it take a dummy type called something likeremember_to_consider_destructor_order_t
. But these only encourage correct behavior rather than enforcing it.repeating_timer_t
,mailbox_t
, etc.) and write a static analyzer script to enforce that regular members come first, then theauto_drainer_t
if any, then subscription-like classes. We could use Clang's Python bindings, orgcc-xml
.auto_drainer_t
and subscription-like classes to be explicitly destroyed. Basically,auto_drainer_t
andsubscription_t
would havestop()
methods, which would be equivalent to their current destructors; if the destructor was called andstop()
had not been called, then they would crash in debug mode. This would force the coder to givemy_class_t
an explicit destructor that callssubs.stop(); drainer.stop()
. Because these would necessarily run before thefoo
andbar
destructors, this solution automatically enforces thatfoo
andbar
can never be accessed during destruction. It's still possible to calldrainer.stop()
beforesubs.stop()
, but it encourages people to think about it.