3
3
#include " scheduler.h"
4
4
#include " private.h"
5
5
6
+ #include < yt/yt/core/actions/invoker_util.h>
6
7
#include < yt/yt/core/misc/relaxed_mpsc_queue.h>
7
8
#include < yt/yt/core/misc/singleton.h>
8
9
@@ -60,6 +61,41 @@ DEFINE_REFCOUNTED_TYPE(TDelayedExecutorEntry)
60
61
61
62
// //////////////////////////////////////////////////////////////////////////////
62
63
64
+ // ! (arkady-e1ppa) MO's/Fence explanation:
65
+ /*
66
+ We want for shutdown to guarantee that no callback is left in any queue
67
+ once it is over. Otherwise, memory leak or a deadlock of Poller/GrpcServer
68
+ (or someone else who blocks thread until some callback is run) will occur.
69
+
70
+ We model our queue with Enqueue being RMW^rel(Queue_, x, y) and Dequeue
71
+ being RMW^acq(Queue_, x, y), where x is what we have read and y is what we
72
+ have observed. Missing callback would imply that in Submit method enqueueing |CB|
73
+ we have observed Stopping_ |false| (e.g. TThread::Start (c) returned |true|)
74
+ but also in ThreadMain during the SubmitQueue drain (f) we have not observed the
75
+ |CB|. Execution is schematically listed below:
76
+ T1(Submit) T2(Shutdown)
77
+ RMW^rel(Queue_, empty, CB) (a) W^rel(Stopping_, true) (d)
78
+ |sequenced-before |simply hb
79
+ Fence^sc (b) Fence^sc (e)
80
+ |sequenced-before |sequenced-before
81
+ R^acq(Stopping_, false) (c) RMW^acq(Queue_, empty, empty) (f)
82
+
83
+ Since (c) reads |false| it must be reading from Stopping_ ctor which is
84
+ W^na(Stopping_, false) which preceedes (d) in modification order. Thus
85
+ (c) must read-from some modification preceding (d) in modification order (ctor)
86
+ and therefore (c) -cob-> (d) (coherence ordered before).
87
+ Likewise, (f) reads |empty| which can only be read from Queue_ ctor or
88
+ prior Dequeue both of which preceede (a) in modification order (ctor is obvious;
89
+ former Dequeue by assumption that no one has read |CB| ever: if some (a) was
90
+ prior to some Dequeue in modification order, |CB| would inevitably be read).
91
+ So, (f) -cob-> (a). For fences we now have to relations:
92
+ (b) -sb-> (c) -cob-> (d) -simply hb-> (e) => (b) -S-> (e)
93
+ (e) -sb-> (f) -cob-> (a) -sb-> (b) => (e) -S-> (b)
94
+ Here sb is sequenced-before and S is sequentially-consistent total ordering.
95
+ We have formed a loop in S thus contradicting the assumption.
96
+ */
97
+
98
+
63
99
class TDelayedExecutorImpl
64
100
{
65
101
public:
@@ -139,9 +175,11 @@ class TDelayedExecutorImpl
139
175
{
140
176
YT_VERIFY (callback);
141
177
auto entry = New<TDelayedExecutorEntry>(std::move (callback), deadline, std::move (invoker));
142
- PollerThread_->EnqueueSubmission (entry);
178
+ PollerThread_->EnqueueSubmission (entry); // <- (a)
179
+
180
+ std::atomic_thread_fence (std::memory_order::seq_cst); // <- (b)
143
181
144
- if (!PollerThread_->Start ()) {
182
+ if (!PollerThread_->Start ()) { // <- (c)
145
183
if (auto callback = TakeCallback (entry)) {
146
184
callback (/* aborted*/ true );
147
185
}
@@ -213,6 +251,43 @@ class TDelayedExecutorImpl
213
251
NProfiling::TCounter CanceledCallbacksCounter_ = ConcurrencyProfiler.Counter(" /delayed_executor/canceled_callbacks" );
214
252
NProfiling::TCounter StaleCallbacksCounter_ = ConcurrencyProfiler.Counter(" /delayed_executor/stale_callbacks" );
215
253
254
+ class TCallbackGuard
255
+ {
256
+ public:
257
+ TCallbackGuard (TCallback<void (bool )> callback, bool aborted) noexcept
258
+ : Callback_(std::move(callback))
259
+ , Aborted_(aborted)
260
+ { }
261
+
262
+ TCallbackGuard (TCallbackGuard&& other) = default ;
263
+
264
+ TCallbackGuard (const TCallbackGuard&) = delete ;
265
+
266
+ TCallbackGuard& operator =(const TCallbackGuard&) = delete ;
267
+ TCallbackGuard& operator =(TCallbackGuard&&) = delete ;
268
+
269
+ void operator ()()
270
+ {
271
+ auto callback = std::move (Callback_);
272
+ YT_VERIFY (callback);
273
+ callback.Run (Aborted_);
274
+ }
275
+
276
+ ~TCallbackGuard ()
277
+ {
278
+ if (Callback_) {
279
+ YT_LOG_DEBUG (" Aborting delayed executor callback" );
280
+
281
+ auto callback = std::move (Callback_);
282
+ callback (/* aborted*/ true );
283
+ }
284
+ }
285
+
286
+ private:
287
+ TCallback<void (bool )> Callback_;
288
+ bool Aborted_;
289
+ };
290
+
216
291
217
292
void StartPrologue () override
218
293
{
@@ -240,6 +315,13 @@ class TDelayedExecutorImpl
240
315
ProcessQueues ();
241
316
242
317
if (IsStopping ()) {
318
+ // We have Stopping_.store(true) in simply happens-before relation.
319
+ // Assume Stopping_.store(true, release) is (d).
320
+ // NB(arkady-e1ppa): At the time of writing it is seq_cst
321
+ // actually, but
322
+ // 1. We don't need it to be for correctness hehe
323
+ // 2. It won't help us here anyway
324
+ // 3. It might be changed as it could be suboptimal.
243
325
break ;
244
326
}
245
327
@@ -251,6 +333,8 @@ class TDelayedExecutorImpl
251
333
EventCount_->Wait (cookie, deadline);
252
334
}
253
335
336
+ std::atomic_thread_fence (std::memory_order::seq_cst); // <- (e)
337
+
254
338
// Perform graceful shutdown.
255
339
256
340
// First run the scheduled callbacks with |aborted = true|.
@@ -267,7 +351,7 @@ class TDelayedExecutorImpl
267
351
// Now we handle the queued callbacks similarly.
268
352
{
269
353
TDelayedExecutorEntryPtr entry;
270
- while (SubmitQueue_.TryDequeue (&entry)) {
354
+ while (SubmitQueue_.TryDequeue (&entry)) { // <- (f)
271
355
runAbort (entry);
272
356
}
273
357
}
@@ -349,7 +433,11 @@ class TDelayedExecutorImpl
349
433
void RunCallback (const TDelayedExecutorEntryPtr& entry, bool abort)
350
434
{
351
435
if (auto callback = TakeCallback (entry)) {
352
- (entry->Invoker ? entry->Invoker : DelayedInvoker_)->Invoke (BIND_NO_PROPAGATE (std::move (callback), abort ));
436
+ auto invoker = entry->Invoker
437
+ ? entry->Invoker
438
+ : DelayedInvoker_;
439
+ invoker
440
+ ->Invoke (BIND_NO_PROPAGATE (TCallbackGuard (std::move (callback), abort )));
353
441
}
354
442
}
355
443
};
0 commit comments