blob: ba03b846ac3ccee78e15b7aa5f7bb2c9078b6a08 [file] [view]
# BE Exec Module — Review Guide
Review changes by their owning module first, then apply the cross-cutting checks. Add new recurring
failure patterns under the corresponding module instead of building one flat checklist.
## Cross-Cutting Execution Rules
### Operator Lifecycle
Tasks move through `INITED → RUNNABLE → BLOCKED → FINISHED → FINALIZED`.
- [ ] `SharedState` source/sink dependencies connected through `inject_shared_state()`?
### Memory Reservation and Spill
- [ ] Memory-heavy operators use `_try_to_reserve_memory()` before materializing large structures?
- [ ] `_memory_sufficient_dependency` wired in where pressure should block, not overrun?
- [ ] `revoke_memory()` preserves the existing spill path?
### Dependency Concurrency
- [ ] Default readiness preserved? Source starts blocked; sink starts ready
- [ ] `set_ready()` fast-path precheck vs `is_blocked_by()` lock-first asymmetry respected?
- [ ] New `Dependency` subclasses pair `block()` / `set_ready()` on every path?
- [ ] `CountedFinishDependency::add()` and `sub()` under `_mtx`?
### Atomics
- [ ] Relaxed atomics only for statistics; lifecycle/stop flags use at least acquire/release?
### Asynchronous BRPC Lifecycle
Async RPC completion callbacks may synchronously start another RPC. Review them as reentrant code,
even when the surrounding queue permits only one network request to be in flight.
#### Checkpoints
- [ ] Request, response, and `brpc::Controller` remain alive until brpc invokes the completion closure?
- [ ] `AutoReleaseClosure` has an external callback owner while the callback result is still needed?
Do not replace its `weak_ptr` with strong ownership that can retain `QueryContext` and other
large query-scoped objects after cancellation.
- [ ] A callback that starts RPC-B avoids resetting or overwriting RPC-A's callback, Controller,
response, or `std::function` target while callback-A is still on the stack? Prefer per-RPC
callback state when reentrant sends are possible.
- [ ] `Controller::Reset()` is called only after the previous RPC and all code reading its result
have finished?
- [ ] Completion releases `request_attachment()` promptly? Runtime-filter attachments can contain
large serialized Bloom filters. If a callback can start another RPC with the same Controller,
clear the old attachment before invoking the callback, never after it.
- [ ] Failure before the RPC is submitted releases any attachment and does not leave a callback
owner retaining its Controller indefinitely?
- [ ] Transport failure, response application status, timeout, and special statuses such as
`END_OF_FILE` preserve the intended cancellation and cleanup semantics?
## Exchange Module
Apply these checks to remote exchange, local exchange, broadcast paths, `ExchangeSinkBuffer`,
`Channel`, `VDataStreamRecvr`, and every `SenderQueue` implementation.
### EOF and Termination
- [ ] When the peer returns `END_OF_FILE`, does the sender stop or turn off the correct channel and
release queued regular and broadcast blocks without treating a normal receiver close as an
unrelated transport failure?
- [ ] Does every EOF path signal all affected dependencies? Check sink queue dependencies, source
dependencies, local-channel dependencies, broadcast dependencies, and finish dependencies;
missing one signal can leave the query blocked forever.
- [ ] Do RPC failure, response-status failure, callback-owner expiration, cancellation, and close
leave `rpc_channel_is_idle` and channel turn-off state consistent, so no queue waits for a
callback that will never run?
- [ ] Does receiver-side EOS decrement the correct sender exactly once, handle duplicate or stale
packets safely, and make the source ready when the final sender finishes?
- [ ] Can a receiver created with zero senders, or closed before dependency wiring, still make its
source dependency ready instead of waiting forever?
### Sender-Side Memory and Backpressure
- [ ] Is memory growth bounded for regular, broadcast, local, and multi-block queues? Check both
byte limits and block/count limits; do not rely on eventual RPC completion as the only bound.
- [ ] Is every enqueue/accounting or holder `acquire()` paired with a dequeue, drop, EOF, failure,
cancellation, or teardown release on all exits?
- [ ] When consumption drops below the configured limit, is the corresponding upstream dependency
made ready promptly? Do not wait for the whole queue to become empty unless that hysteresis is
intentional and documented.
- [ ] Are `_total_queue_size`, memory counters, queue capacities, and
`BroadcastPBlockHolderMemLimiter` updated under the correct lock without underflow or double
release when one broadcast block is shared by multiple channels?
- [ ] After a receiver EOF or RPC failure discards buffered data, are both memory accounting and
upstream backpressure released?
### Stream Receiver Memory and Backpressure
- [ ] Does each `SenderQueue` enforce `_sender_queue_mem_limit` and the receiver-wide exchange
buffer limit for both serialized remote `PBlock`s and local `Block`s?
- [ ] When a remote queue exceeds its limit and retains the RPC `done` callback as backpressure, is
that callback invoked exactly once after consumption, cancellation, close, destruction, or a
deserialize/error path? A missed callback leaves the remote sender permanently blocked.
- [ ] Are `add_blocks_memory_usage()` and `sub_blocks_memory_usage()` balanced on normal reads and
every failure path, and does dropping below the limit promptly set the local-channel
dependency ready?
- [ ] Does `_source_dependency` become ready when data arrives, the last sender reaches EOS, or the
queue is cancelled/closed, and become blocked only when the queue is empty with live senders?
- [ ] For merging receivers with one queue per sender, are memory limits and dependency signals
correct per queue and in aggregate, without one quiet sender stalling ready senders?
### Exchange RPC Callbacks
- [ ] A completion callback that sends the next queued packet uses separate per-RPC callback,
Controller, response, and handler state?
- [ ] The callback remains alive until its result is handled without making an in-flight RPC retain
the whole query after cancellation?
- [ ] Normal and broadcast branches implement the same failure, EOF, queue-accounting, and
dependency-release behavior?
## Runtime Filter Module
Runtime-filter producers, local mergers, and the global merge node coordinate across parallel
instances and recursive CTE stages. A callback often represents only the producer that sent the
RPC, not every producer waiting for the result.
### Locking and Deadlock Prevention
Manager, producer, merger, dependency, query-cancellation, and RPC callback code execute on
different threads and can call back into one another. A `recursive_mutex` prevents only same-thread
self-deadlock; it does not prevent cross-thread lock inversion.
- [ ] Is there one documented lock order among the manager map lock, `GlobalMergeContext::mtx`,
producer/consumer `_rmtx`, merger locks, and dependency locks? New code must not introduce a
reverse acquisition path.
- [ ] Does manager code release its map/context lock before calling a producer or consumer method
that takes `_rmtx`, if producer/consumer code can call back into the manager? Review the full
call graph, not only the locks visible in the changed function.
- [ ] Does producer code avoid holding `_rmtx` while entering manager/merger code when another path
can hold a manager/merger lock and call `set_synced_size()`, `signal()`, `publish()`, or another
producer method?
- [ ] Is any synchronous RPC, `brpc::Join()`, dependency wait, DNS/stub lookup, or other blocking
operation performed while holding a lock needed by its completion callback? The callback
cannot signal completion if it must acquire the lock held by the waiting thread.
- [ ] Are query cancellation, dependency `sub()`/`set_ready()`/`set_always_ready()`, RPC callbacks,
and destruction of callback/controller owners invoked outside manager and producer locks when
they may wake tasks, run cleanup, or re-enter runtime-filter code?
- [ ] When avoiding a race by extending a lock across merge/publish/reset, does the protected region
avoid calling code that can synchronously re-enter the same context or acquire locks in the
opposite order? Prefer taking an immutable stage snapshot or per-RPC owner when practical.
- [ ] Do all early returns and exceptions release locks before waiting for cleanup that needs those
locks, and are `Defer` actions reviewed for what they execute while local lock guards exist?
A classic inversion to reject during review is:
```text
Thread A: Producer::_rmtx -> RuntimeFilterMgr lock
Thread B: RuntimeFilterMgr lock -> producer->set_synced_size() -> Producer::_rmtx
```
Another common callback deadlock is:
```text
Thread A: holds GlobalMergeContext::mtx -> waits for RPC/dependency completion
Thread B: RPC completion callback -> needs GlobalMergeContext::mtx to signal completion
```
### Producer and Dependency Coordination
- [ ] Every `CountedFinishDependency::add()` has exactly one matching `sub()` or a query-cancel path
that unblocks all affected dependencies? Check all parallel producers, not only the final
producer that sends the size RPC.
- [ ] Error and `END_OF_FILE` handling cannot release only the sender's dependency while sibling
producer dependencies remain blocked?
- [ ] Runtime-filter RPC timeouts use the intended timeout domain (runtime-filter wait versus query
execution timeout), with the choice documented?
### Merge and Recursive CTE Stages
- [ ] Recursive-stage validation and the operation it protects are atomic with respect to reset?
Do not check `stage` under `GlobalMergeContext::mtx`, unlock, and then read or mutate the same
stage's merger, `done`, targets, or callback storage.
- [ ] `publish_callbacks` and `sync_size_callbacks` resize/write/clear operations are synchronized
with recursive reset, and an old stage cannot repopulate a new stage's callback owners?