blob: a4e585caf7d825f3f0ffec932184d51dda6278aa [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#pragma once
#include <mutex>
#include "exec/data-sink.h"
#include "exec/filter-context.h"
#include "runtime/runtime-state.h"
#include "util/condition-variable.h"
namespace impala {
class NljBuilder;
class PhjBuilder;
class IcebergDeleteBuilder;
class JoinBuilderConfig : public DataSinkConfig {
public:
~JoinBuilderConfig() override {}
protected:
friend class JoinBuilder;
friend class NljBuilder;
friend class PhjBuilder;
friend class IcebergDeleteBuilder;
Status Init(const TDataSink& tsink, const RowDescriptor* input_row_desc,
FragmentState* state) override;
/// The ID of the join plan node this is associated with.
int join_node_id_;
/// The join operation this is building for.
TJoinOp::type join_op_;
};
/// Join builder for use with BlockingJoinNode.
///
/// Implements the DataSink interface but also exposes some methods for direct use by
/// BlockingJoinNode, e.g. the implemention of the protocol to hand off completed
/// builds to the join node when the plan has a separate join fragment.
///
/// Example of Synchronization
/// --------------------------
/// The below sequence diagram shows an example of how the probe and build finstance
/// threads synchronize to do the nested loop join build, hand it off to the probe thread
/// and then release resources at the end. For simplicitly, this particular sequence
/// assumes that Prepare() was already called on the build finstance thread so
/// GetFInstanceState() returns immediately (otherwise Open() would block waiting for
/// the build finstance to be prepared).
///
///
/// +------------------+ +--------------------+ +-------------+ +-----------------+
/// | probe finstance | | NestedLoopJoinNode | | NljBuilder | | build finstance |
/// | thread | | | | | | thread |
/// +------------------+ +--------------------+ +-------------+ +-----------------+
/// | | | |
/// | Open() | | |
/// |------------>| | |
/// | | | Open() |
/// | | |<----------------------------|
/// | | | Send() multiple times |
/// | | |<----------------------------|
/// | | WaitForInitialBuild() | |
/// | | blocks on probe_wakeup_cv_ | |
/// | |----------------------------->| |
/// | | | Send() multiple times |
/// | | |<----------------------------|
/// | | | FlushFinal() |
/// | | | calls HandoffToProbesAndWait()
/// | | | signals probe_wakeup_cv_ |
/// | | | blocks on build_wakeup_cv_ |
/// | | |<----------------------------|
/// | | WaitForInitialBuild() returns| |
/// | |<-----------------------------| |
/// | | | |
/// | | accesses data structures in | |
/// | | read-only manner | |
/// | |----------------------------->| |
/// | | | |
/// | | CloseFromProbe() | |
/// | | signals build_wakeup_cv_ | |
/// | |----------------------------->| |
/// | | | FlushFinal() returns |
/// | | |---------------------------->|
/// | | | |
/// | | | Close() |
/// | | |<----------------------------|
///
/// Various alternative flows are possible:
/// * WaitForInitialBuild() may be called after FlushFinal(), in which case it can return
/// immediately.
/// * The query may be cancelled while the threads are blocked in WaitForInitialBuild()
/// or HandoffToProbesAndWait(). In this case the threads need to get unblocked in a
/// safe manner. Details are documented in the respective functions.
///
/// Other implementations, such as the partitioned hash join, may do additional
/// synchronization in between WaitForInitialBuild() returning and CloseFromProbe() being
/// called, instead of the simple read-only access of the NLJ. The details are are left
/// to the specific implementations, but must be thread-safe if the builder is shared
/// between multiple threads.
class JoinBuilder : public DataSink {
public:
/// Construct the join builder. This is a separate build if 'sink_id' is valid, or
/// an integrated build if 'sink_id' is -1.
JoinBuilder(TDataSinkId sink_id, const JoinBuilderConfig& sink_config,
const string& name, RuntimeState* state);
virtual ~JoinBuilder();
/// Waits for a separate join build to complete and be ready for probing. Always called
/// from a BlockingJoinNode subclass in a different fragment instance.
///
/// Returns OK if the initial build succeeded. Returns CANCELLED if the join node's
/// fragment is cancelled. If an error occurs executing the build side of the join,
/// we rely on query-wide cancellation to cancel the probe side of the join, instead
/// of propagating the error/cancellation via the JoinBuilder. In case of cancellation
/// or error, this function blocks until the join node's finstance is cancelled so
/// that the CANCELLED status does not race with the root cause of the query failure.
Status WaitForInitialBuild(RuntimeState* join_node_state);
/// This is called from BlockingJoinNode to signal that the node is done with the
/// builder. If this is an embedded join build, this simply closes the builder. If it is
/// a separate join build, each BlockingJoinNode that called WaitForInitialBuild() needs
/// to call CloseFromProbe(). The builder is closed when the last BlockingJoinNode using
/// this builder calls CloseFromProbe(). BlockingJoinNode never calls Close() directly.
void CloseFromProbe(RuntimeState* join_node_state);
/// This is called from BlockingJoinNode to signal that the node won't even reach the
/// probe phase. This can happen if the node is closed before calling Open(). This can
/// happen if the BlockingJoinNode is a child of a TupleCacheNode with a cache hit.
/// This only makes sense when using a separate join build. Each BlockingJoinNode needs
/// to call CloseBeforeProbe() if it won't call WaitForInitialBuild(). This adjusts the
/// number of outstanding probes so that the builder doesn't wait unnecessarily for
/// probes that will never show up. If there are no threads left, this notifies the
/// builder.
void CloseBeforeProbe(RuntimeState* join_node_state);
int num_probe_threads() const { return num_probe_threads_; }
static string ConstructBuilderName(const char* name, int join_node_id) {
return strings::Substitute("$0 Join Builder (join_node_id=$1)", name, join_node_id);
}
protected:
/// ID of the join node that this builder is associated with.
const int join_node_id_;
/// The join operation this is building for.
const TJoinOp::type join_op_;
/// True if this is a separate DataSink at the root of its own fragment. Otherwise this
/// is embedded in a PartitionedHashJoinNode.
const bool is_separate_build_;
/// Number of build rows. Initialized in Prepare().
RuntimeProfile::Counter* num_build_rows_ = nullptr;
/////////////////////////////////////////////////////////////////////
/// BEGIN: Members that are used only when is_separate_build_ is true
// Lock used for synchronization between threads from the build and probe side (i.e.
// the build fragment thread and the probe-side thread executing the join node).
std::mutex separate_build_lock_;
// Probe-side threads block on this while waiting for initial_build_complete_ = true
// (or for probe finstance cancellation).
// Protected by 'separate_build_lock_'.
ConditionVariable probe_wakeup_cv_;
// The build-side thread blocks on this while waiting for 'outstanding_probes_' and
// 'probe_refcount_' to go to zero (or for build finstance cancellation).
// Protected by 'separate_build_lock_'.
ConditionVariable build_wakeup_cv_;
// Set to true when the builder is ready for the probe side to use. Set to true in
// FlushFinal().
// Protected by 'separate_build_lock_'.
bool ready_to_probe_ = false;
// Total number of probe-side fragment instances, i.e. threads.
// Always 1 if 'is_separate_build_' is false.
const int num_probe_threads_;
// Number of probe-side threads that are expected to call WaitForInitialBuild()
// but have not yet called it.
// Protected by 'separate_build_lock_'.
int outstanding_probes_ = 0;
// Number of probe-side threads using this builder - i.e. WaitForInitialBuild() calls
// without a corresponding CloseBuilder() call.
// Protected by 'separate_build_lock_'.
int probe_refcount_ = 0;
/// END: Members that are used only when is_separate_build_ is true
/////////////////////////////////////////////////////////////////////
/// Called by the build-side thread of a separate join build once the initial build has
/// been completed and it is ready to hand off to probe-side threads. Blocks until the
/// probe side is finished using the build, or the build finstance is cancelled. When
/// this returns, no probe-side threads will be using the build and
/// WaitForInitialBuild() will return CANCELLED.
///
/// This should be called by the FlushFinal() method of the subclass, after all other
/// resources from the build fragment have been released.
/// TODO: IMPALA-9255: reconsider this so that the build-side thread can exit instead
/// of being blocked indefinitely.
void HandoffToProbesAndWait(RuntimeState* build_side_state);
/// Publish the runtime filters as described in 'filter_ctxs' to the fragment-local
/// RuntimeFilterBank in 'runtime_state'. 'minmax_filter_threshold' specifies the
/// threshold to determine the usefulness of a min/max filter. 'num_build_rows' is used
/// to determine whether the computed filters have an unacceptably high false-positive
/// rate.
void PublishRuntimeFilters(const std::vector<FilterContext>& filter_ctxs,
RuntimeState* runtime_state, float minmax_filter_threshold, int64_t num_build_rows);
};
}