|
35 | 35 | #include "compute_env/spill/serde.h" |
36 | 36 | #include "compute_env/spill/spill_components.h" |
37 | 37 | #include "compute_env/spill/spill_metrics.h" |
| 38 | +#include "compute_env/spill/spill_observable.h" |
38 | 39 | #include "compute_env/spill/spiller_factory.h" |
39 | 40 | #include "compute_env/spill/task_executor.h" |
40 | 41 | #include "fs/fs.h" |
41 | 42 | #include "gen_cpp/InternalService_types.h" |
| 43 | +#include "gutil/macros.h" |
42 | 44 | #include "runtime/mem_tracker.h" |
43 | 45 | #include "runtime/runtime_state_fwd.h" |
44 | 46 |
|
@@ -169,6 +171,45 @@ class Spiller : public std::enable_shared_from_this<Spiller> { |
169 | 171 |
|
170 | 172 | const SpillProcessMetrics& metrics() { return _metrics; } |
171 | 173 |
|
| 174 | + // Notification layer. It lives on the Spiller and survives reset_state, so the observer lists outlive |
| 175 | + // reallocation of the writer/reader. |
| 176 | + SpillEventObservable& observable() { return _observable; } |
| 177 | + |
| 178 | + void notify_sink_observers() { _observable.notify_sink_observers(); } |
| 179 | + void notify_source_observers() { _observable.notify_source_observers(); } |
| 180 | + |
| 181 | + // The single place that owns the completion order for spiller IO: |
| 182 | + // publish -> decrement the in-flight count -> notify. |
| 183 | + // The notify runs after both the predicates (published by publish_fn) and the count are visible, so one |
| 184 | + // notify serves every sleeper: the OUTPUT_FULL/INPUT_EMPTY predicate sleepers re-check predicates that |
| 185 | + // are already open, and a PENDING_FINISH driver gating on the count sees it already at its new value. |
| 186 | + // The caller must keep the notified observers alive across the call: a task body calls this through |
| 187 | + // defer_complete_io (the notify runs inside the task's query-lifetime guard window), and the |
| 188 | + // submit-failure compensation calls it directly on the pipeline thread, where the executing driver |
| 189 | + // itself keeps the query alive. Every completion path goes through here. |
| 190 | + template <class PublishFn> |
| 191 | + void complete_io(PublishFn&& publish_fn, bool wake_sink) { |
| 192 | + std::forward<PublishFn>(publish_fn)(); |
| 193 | + decrease_in_flight_io(); |
| 194 | + if (wake_sink) { |
| 195 | + notify_sink_observers(); |
| 196 | + } |
| 197 | + notify_source_observers(); |
| 198 | + } |
| 199 | + |
| 200 | + // RAII form of complete_io for IO task bodies; see IOCompletion below. The guard must already be |
| 201 | + // scoped_begin'd; the returned object runs the completion and then guard.scoped_end() at scope exit. |
| 202 | + template <class PublishFn, class Guard> |
| 203 | + [[nodiscard]] auto defer_complete_io(PublishFn publish_fn, bool wake_sink, const Guard& guard); |
| 204 | + |
| 205 | + // Counts in-flight IO across writer (flush) and reader (restore) tasks. It is in addition to the |
| 206 | + // per-writer/reader counters, which stay as lifetime gates for transient readers. It is incremented |
| 207 | + // before submit at every choke point, and decremented in the same defer as the per-task counter. |
| 208 | + void increase_in_flight_io() { _in_flight_io.fetch_add(1, std::memory_order_acq_rel); } |
| 209 | + void decrease_in_flight_io() { _in_flight_io.fetch_sub(1, std::memory_order_acq_rel); } |
| 210 | + |
| 211 | + bool has_running_io_tasks() const { return _in_flight_io.load(std::memory_order_acquire) > 0; } |
| 212 | + |
172 | 213 | // set partitions for spiller only works when spiller has partitioned spill writer |
173 | 214 | void set_partition(const std::vector<const SpillPartitionInfo*>& parititons); |
174 | 215 | // init partition by `num_partitions` |
@@ -303,6 +344,54 @@ class Spiller : public std::enable_shared_from_this<Spiller> { |
303 | 344 | size_t _max_sorted_block_cnt = 0; |
304 | 345 | std::atomic_bool _is_cancel = false; |
305 | 346 | std::atomic_bool _global_spill_triggered{false}; |
| 347 | + |
| 348 | + SpillEventObservable _observable; |
| 349 | + // Count of in-flight flush/restore IO tasks. It survives reset_state (it lives on the Spiller, not on |
| 350 | + // the writer/reader), so it can gate teardown and re-prepare against tasks that captured raw |
| 351 | + // writer/reader pointers. |
| 352 | + std::atomic<int64_t> _in_flight_io{0}; |
| 353 | +}; |
| 354 | + |
| 355 | +// RAII completion for an IO task body, returned by Spiller::defer_complete_io. At scope exit it runs the |
| 356 | +// completion (complete_io: publish -> decrement -> notify) and then the guard's scoped_end, in that order. |
| 357 | +// The order matters for correctness, not for style: scoped_end releases the query-lifetime pin the guard |
| 358 | +// took in scoped_begin, and that pin is what keeps the notified observers alive -- if scoped_end ran first, |
| 359 | +// the notify could dereference freed observers. Do not reorder. cancel_for_yield() drops the completion AND |
| 360 | +// marks the yield context unfinished in one call (on a yield-resubmit the task lives on, keeps its |
| 361 | +// in-flight increment, and re-enters scoped_begin on its next run); scoped_end still runs to balance this |
| 362 | +// invocation's begin. The two cancels are coupled on purpose: cancelling the completion without cancelling |
| 363 | +// the yield defer would let the executor drop the task with the in-flight count still held. |
| 364 | +template <class PublishFn, class Guard> |
| 365 | +class IOCompletion { |
| 366 | +public: |
| 367 | + IOCompletion(Spiller* spiller, PublishFn publish_fn, bool wake_sink, const Guard& guard) |
| 368 | + : _spiller(spiller), _publish_fn(std::move(publish_fn)), _wake_sink(wake_sink), _guard(guard) {} |
| 369 | + ~IOCompletion() noexcept { |
| 370 | + if (!_cancelled) { |
| 371 | + _spiller->complete_io(_publish_fn, _wake_sink); |
| 372 | + } |
| 373 | + _guard.scoped_end(); |
| 374 | + } |
| 375 | + template <class YieldDefer> |
| 376 | + void cancel_for_yield(YieldDefer& yield_defer) { |
| 377 | + _cancelled = true; |
| 378 | + yield_defer.cancel(); |
| 379 | + } |
| 380 | + DISALLOW_COPY_AND_MOVE(IOCompletion); |
| 381 | + |
| 382 | +private: |
| 383 | + Spiller* _spiller; |
| 384 | + PublishFn _publish_fn; |
| 385 | + const bool _wake_sink; |
| 386 | + const Guard& _guard; |
| 387 | + bool _cancelled = false; |
306 | 388 | }; |
307 | 389 |
|
| 390 | +template <class PublishFn, class Guard> |
| 391 | +[[nodiscard]] auto Spiller::defer_complete_io(PublishFn publish_fn, bool wake_sink, const Guard& guard) { |
| 392 | + // Returned as a prvalue: C++17 guaranteed elision constructs it in the caller's variable, so the |
| 393 | + // deleted copy/move constructors are never used. |
| 394 | + return IOCompletion<PublishFn, Guard>(this, std::move(publish_fn), wake_sink, guard); |
| 395 | +} |
| 396 | + |
308 | 397 | } // namespace starrocks::spill |
0 commit comments