| // 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. |
| |
| #include <gflags/gflags.h> |
| |
| #include "exec/exec-node-util.h" |
| #include "exec/hdfs-scan-node-base.h" |
| #include "exec/tuple-cache-node.h" |
| #include "exec/tuple-file-reader.h" |
| #include "exec/tuple-file-writer.h" |
| #include "exec/tuple-text-file-reader.h" |
| #include "exec/tuple-text-file-util.h" |
| #include "exec/tuple-text-file-writer.h" |
| #include "runtime/exec-env.h" |
| #include "runtime/row-batch.h" |
| #include "runtime/runtime-state.h" |
| #include "runtime/tuple-cache-mgr.h" |
| #include "util/hash-util.h" |
| #include "util/runtime-profile-counters.h" |
| #include "util/runtime-profile.h" |
| |
| #include "common/names.h" |
| |
| namespace impala { |
| |
| Status TupleCachePlanNode::CreateExecNode( |
| RuntimeState* state, ExecNode** node) const { |
| ObjectPool* pool = state->obj_pool(); |
| *node = pool->Add(new TupleCacheNode(pool, *this, state->desc_tbl())); |
| return Status::OK(); |
| } |
| |
| TupleCacheNode::TupleCacheNode( |
| ObjectPool* pool, const TupleCachePlanNode& pnode, const DescriptorTbl& descs) |
| : ExecNode(pool, pnode, descs) { |
| } |
| |
| TupleCacheNode::~TupleCacheNode() = default; |
| |
| static bool TupleCacheVerificationEnabled(RuntimeState* state) { |
| DCHECK(state != nullptr); |
| return state->query_options().enable_tuple_cache_verification; |
| } |
| |
| Status TupleCacheNode::Prepare(RuntimeState* state) { |
| RETURN_IF_ERROR(ExecNode::Prepare(state)); |
| num_hits_counter_ = ADD_COUNTER(runtime_profile(), "NumTupleCacheHits", TUnit::UNIT); |
| num_halted_counter_ = |
| ADD_COUNTER(runtime_profile(), "NumTupleCacheHalted", TUnit::UNIT); |
| num_backpressure_halted_counter_ = |
| ADD_COUNTER(runtime_profile(), "NumTupleCacheBackpressureHalted", TUnit::UNIT); |
| num_skipped_counter_ = |
| ADD_COUNTER(runtime_profile(), "NumTupleCacheSkipped", TUnit::UNIT); |
| // If correctness verification is enabled, add a counter to indicate whether it was |
| // actually performing correctness verification. |
| if (TupleCacheVerificationEnabled(state)) { |
| num_correctness_verification_counter_ = |
| ADD_COUNTER(runtime_profile(), "NumTupleCacheCorrectnessVerification", TUnit::UNIT); |
| } |
| // Compute the combined cache key by computing the fragment instance key and |
| // fusing it with the compile time key. |
| ComputeFragmentInstanceKey(state); |
| combined_key_ = plan_node().tnode_->tuple_cache_node.compile_time_key + "_" + |
| std::to_string(fragment_instance_key_); |
| skip_correctness_verification_ = |
| plan_node().tnode_->tuple_cache_node.skip_correctness_verification; |
| runtime_profile()->AddInfoString("Combined Key", combined_key_); |
| |
| return Status::OK(); |
| } |
| |
| Status TupleCacheNode::Open(RuntimeState* state) { |
| SCOPED_TIMER(runtime_profile()->total_time_counter()); |
| ScopedOpenEventAdder ea(this); |
| RETURN_IF_ERROR(ExecNode::Open(state)); |
| |
| // The frontend cannot create a TupleCacheNode if enable_tuple_cache=false |
| // Fail the query if we see this. |
| if (!state->query_options().enable_tuple_cache) { |
| return Status("Invalid tuple caching configuration: enable_tuple_cache=false"); |
| } |
| |
| TupleCacheMgr* tuple_cache_mgr = ExecEnv::GetInstance()->tuple_cache_mgr(); |
| handle_ = tuple_cache_mgr->Lookup(combined_key_, true); |
| if (tuple_cache_mgr->IsAvailableForRead(handle_)) { |
| if (tuple_cache_mgr->DebugDumpEnabled() && TupleCacheVerificationEnabled(state)) { |
| // If the node is marked to skip correctness verification, we don't want to read |
| // from the cache as that would prevent its children from executing. |
| if (!skip_correctness_verification_) { |
| VLOG_FILE << "Tuple Cache: correctness verification for " << combined_key_; |
| DCHECK(num_correctness_verification_counter_ != nullptr); |
| COUNTER_ADD(num_correctness_verification_counter_, 1); |
| // We need the original fragment id to construct the path for the reference debug |
| // cache file. If it's missing from the metadata, we return an error status |
| // immediately. |
| string org_fragment_id = |
| tuple_cache_mgr->GetFragmentIdForTupleCache(combined_key_); |
| if (org_fragment_id.empty()) { |
| return Status(TErrorCode::TUPLE_CACHE_INCONSISTENCY, |
| Substitute("Metadata of tuple cache '$0' is missing for correctness check", |
| combined_key_)); |
| } |
| string ref_sub_dir; |
| string sub_dir; |
| string ref_file_path = GetDebugDumpPath(state, org_fragment_id, &ref_sub_dir); |
| string file_path = GetDebugDumpPath(state, string(), &sub_dir); |
| DCHECK_EQ(ref_sub_dir, sub_dir); |
| DCHECK(!ref_sub_dir.empty()); |
| DCHECK(!ref_file_path.empty()); |
| DCHECK(!file_path.empty()); |
| // Create the subdirectory for the debug caches if needed. |
| RETURN_IF_ERROR(tuple_cache_mgr->CreateDebugDumpSubdir(ref_sub_dir)); |
| // Open the writer for writing the tuple data from the cache entries to be |
| // the reference cache data. |
| debug_dump_text_writer_ref_ = make_unique<TupleTextFileWriter>(ref_file_path); |
| RETURN_IF_ERROR(debug_dump_text_writer_ref_->Open()); |
| // Open the writer for writing the tuple data from children in GetNext() to |
| // compare with the reference debug cache file. |
| debug_dump_text_writer_ = make_unique<TupleTextFileWriter>(file_path); |
| RETURN_IF_ERROR(debug_dump_text_writer_->Open()); |
| } |
| } else { |
| reader_ = make_unique<TupleFileReader>( |
| tuple_cache_mgr->GetPath(handle_), mem_tracker(), runtime_profile()); |
| Status status = reader_->Open(state); |
| // Clear reader if it's not usable |
| if (!status.ok()) { |
| LOG(WARNING) << "Could not read cache entry for " |
| << tuple_cache_mgr->GetPath(handle_); |
| reader_.reset(); |
| } |
| } |
| } else if (tuple_cache_mgr->IsAvailableForWrite(handle_)) { |
| writer_ = make_unique<TupleFileWriter>(tuple_cache_mgr->GetPath(handle_), |
| mem_tracker(), runtime_profile(), |
| [this, tuple_cache_mgr] (size_t new_size) { |
| return tuple_cache_mgr->RequestWriteSize(&this->handle_, new_size); |
| }); |
| Status status = writer_->Open(state); |
| if (!status.ok()) { |
| LOG(WARNING) << "Could not write cache entry for " |
| << tuple_cache_mgr->GetPath(handle_); |
| tuple_cache_mgr->AbortWrite(move(handle_), false); |
| writer_.reset(); |
| } |
| } |
| |
| if (reader_) { |
| COUNTER_ADD(num_hits_counter_, 1); |
| tuple_cache_mgr->IncrementMetric(TupleCacheMgr::MetricType::HIT); |
| } else { |
| if (!writer_) { |
| // May be skipped due to any of: |
| // - the query requests caching but cache is disabled via startup option |
| // - another fragment is currently writing this cache entry |
| // - the cache entry is a tombstone to prevent retries for too large entries |
| VLOG_FILE << "Tuple Cache: skipped for " << combined_key_; |
| COUNTER_ADD(num_skipped_counter_, 1); |
| tuple_cache_mgr->IncrementMetric(TupleCacheMgr::MetricType::SKIPPED); |
| } |
| tuple_cache_mgr->IncrementMetric(TupleCacheMgr::MetricType::MISS); |
| // No reader, so open the child. |
| RETURN_IF_ERROR(child(0)->Open(state)); |
| } |
| |
| // Claim reservation after the child has been opened to reduce the peak reservation |
| // requirement. |
| if (!buffer_pool_client()->is_registered()) { |
| RETURN_IF_ERROR(ClaimBufferReservation(state)); |
| } |
| |
| return Status::OK(); |
| } |
| |
| // Helper function to rename the bad file. |
| static void MoveBadDebugCacheFile(const string& file_path) { |
| DCHECK(!file_path.empty()); |
| string new_path = file_path + DEBUG_TUPLE_CACHE_BAD_POSTFIX; |
| int result = rename(file_path.c_str(), new_path.c_str()); |
| if (result != 0) { |
| string error_msg = GetStrErrMsg(); |
| LOG(ERROR) << "Failed to move debug tuple cache file from " << file_path << " to " |
| << new_path << ". Error message: " << error_msg << ": " << result; |
| } else { |
| LOG(INFO) << "Moved bad debug tuple cache file from " << file_path << " to " |
| << new_path; |
| } |
| } |
| |
| // Move the debug dump cache after verification. |
| // If the verification passed, we clear the cache file. |
| // Otherwise, we will move the file with a "bad" postfix. |
| // The writer will be reset after the function. |
| static void MoveDebugCache(bool suc, unique_ptr<TupleTextFileWriter>& writer) { |
| DCHECK(writer != nullptr); |
| if (suc) { |
| writer->Delete(); |
| } else { |
| MoveBadDebugCacheFile(writer->GetPath()); |
| } |
| writer.reset(); |
| } |
| |
| Status TupleCacheNode::VerifyAndMoveDebugCache(RuntimeState* state) { |
| DCHECK(debug_dump_text_writer_ref_ != nullptr); |
| DCHECK(ExecEnv::GetInstance()->tuple_cache_mgr()->DebugDumpEnabled()); |
| DCHECK(TupleCacheVerificationEnabled(state)); |
| if (debug_dump_text_writer_->IsEmpty()) { |
| return Status::OK(); |
| } |
| string ref_file_path = debug_dump_text_writer_ref_->GetPath(); |
| string dump_file_path = debug_dump_text_writer_->GetPath(); |
| bool passed = false; |
| |
| DCHECK(!ref_file_path.empty()); |
| DCHECK(!dump_file_path.empty()); |
| |
| VLOG_FILE << "Verify debug tuple cache file ref_file_path: " << ref_file_path |
| << " and dump_file_path: " << dump_file_path |
| << " with cache key:" << combined_key_; |
| |
| // Fast path to verify the cache. |
| Status verify_status = |
| TupleTextFileUtil::VerifyDebugDumpCache(dump_file_path, ref_file_path, &passed); |
| if (verify_status.ok() && !passed) { |
| // Slow path to compare all rows in an order-insensitive way if the files are not the |
| // same. |
| verify_status = TupleTextFileUtil::VerifyRows(dump_file_path, ref_file_path); |
| passed = verify_status.ok(); |
| } |
| |
| // Move or clear the file after verification. |
| MoveDebugCache(passed, debug_dump_text_writer_ref_); |
| MoveDebugCache(passed, debug_dump_text_writer_); |
| return verify_status; |
| } |
| |
| // Helper function to generate the unique id based on fragment instance id and node id. |
| static string GenerateFragmentNodeId( |
| const RuntimeState* state, const TupleCacheNode* node) { |
| DCHECK(state != nullptr); |
| DCHECK(node != nullptr); |
| return Substitute("$0_$1", PrintId(state->fragment_instance_id()), node->id()); |
| } |
| |
| Status TupleCacheNode::GetNext( |
| RuntimeState* state, RowBatch* output_row_batch, bool* eos) { |
| SCOPED_TIMER(runtime_profile()->total_time_counter()); |
| ScopedGetNextEventAdder ea(this, eos); |
| RETURN_IF_ERROR(ExecDebugAction(TExecNodePhase::GETNEXT, state)); |
| RETURN_IF_CANCELLED(state); |
| RETURN_IF_ERROR(QueryMaintenance(state)); |
| |
| // Save the number of rows in case GetNext() is called with a non-empty batch, |
| // which can happen in a subplan. |
| int num_rows_before = output_row_batch->num_rows(); |
| |
| // If we have a Reader, return the next batch from it. |
| // Else GetNext from child, write to Writer, and return the batch. |
| if (reader_) { |
| Status status = reader_->GetNext(state, buffer_pool_client(), output_row_batch, eos); |
| if (status.ok()) { |
| cached_rowbatch_returned_to_caller_ = true; |
| // Close the child now that there is no hope of recovery in case of failure. This |
| // allows it to tear down any state and notify any affected threads that the |
| // children won't ever reach Open(). |
| child(0)->Close(state); |
| } else { |
| // If we have returned a cached row batch to the caller, then it is not safe |
| // to try to get any rows from the child as they could be duplicates. Any |
| // error needs to end the query. |
| if (cached_rowbatch_returned_to_caller_) return status; |
| |
| // We haven't returned a RowBatch to the caller yet, so we can recover by aborting |
| // the read from the cache and fetching from the child. We won't try to write to |
| // the cache. |
| LOG(WARNING) << "Unable to read cache file: " << status.GetDetail() |
| << "Falling back to regular non-cached path."; |
| reader_.reset(); |
| // If reader_ is set, then the child was never opened and needs to be opened now |
| RETURN_IF_ERROR(child(0)->Open(state)); |
| RETURN_IF_ERROR(child(0)->GetNext(state, output_row_batch, eos)); |
| } |
| } else { |
| RETURN_IF_ERROR(child(0)->GetNext(state, output_row_batch, eos)); |
| if (writer_) { |
| Status status = writer_->Write(state, output_row_batch); |
| TupleCacheMgr* tuple_cache_mgr = ExecEnv::GetInstance()->tuple_cache_mgr(); |
| // If there was an error or we exceeded the file size limit, stop caching but |
| // continue reading from the child node. |
| if (!status.ok()) { |
| bool set_tombstone = false; |
| if (status.code() == TErrorCode::TUPLE_CACHE_ENTRY_SIZE_LIMIT_EXCEEDED) { |
| VLOG_FILE << "Tuple Cache entry for " << combined_key_ |
| << " hit the maximum file size: " << status.GetDetail(); |
| COUNTER_ADD(num_halted_counter_, 1); |
| set_tombstone = true; |
| } else if (status.code() == |
| TErrorCode::TUPLE_CACHE_OUTSTANDING_WRITE_LIMIT_EXCEEDED) { |
| VLOG_FILE << "Tuple Cache entry for " << combined_key_ |
| << " hit the outstanding writes limit: " << status.GetDetail(); |
| COUNTER_ADD(num_backpressure_halted_counter_, 1); |
| } else { |
| // This is an unknown error (e.g. an IO error), so write a warning. |
| LOG(WARNING) << "Unable to write cache file: " << status.GetDetail(); |
| } |
| writer_->Abort(); |
| tuple_cache_mgr->AbortWrite(move(handle_), set_tombstone); |
| writer_.reset(); |
| } else if (*eos) { |
| // If we hit end of stream, then we can complete the cache entry |
| // If the child did not reach end of stream, then it clearly isn't the complete |
| // result set. This is currently the only way a cache entry can be completed. |
| size_t bytes_written = writer_->BytesWritten(); |
| Status status = writer_->Commit(state); |
| if (status.ok()) { |
| if (tuple_cache_mgr->DebugDumpEnabled()) { |
| // Store the metadata whenever the debug dump path is set, regardless of |
| // whether the correctness verification is enabled in the query option. This |
| // is because the tuple cache eviction function does not account for the query |
| // option when removing metadata. Keeping this consistent ensures proper |
| // handling. |
| tuple_cache_mgr->StoreMetadataForTupleCache( |
| combined_key_, GenerateFragmentNodeId(state, this)); |
| } |
| tuple_cache_mgr->CompleteWrite(move(handle_), bytes_written); |
| } else { |
| writer_->Abort(); |
| tuple_cache_mgr->AbortWrite(move(handle_), false); |
| } |
| writer_.reset(); |
| } |
| } |
| if (debug_dump_text_writer_) { |
| RETURN_IF_ERROR(debug_dump_text_writer_->Write(output_row_batch)); |
| if (*eos) debug_dump_text_writer_->Commit(); |
| } |
| } |
| |
| // Note: TupleCacheNode does not alter its child's output (or the equivalent |
| // output from the cache), so it does not enforce its own limit on the output. |
| // Any limit should be enforced elsewhere, and this code omits the logic |
| // to enforce a limit. |
| int num_rows_added = output_row_batch->num_rows() - num_rows_before; |
| DCHECK_GE(num_rows_added, 0); |
| IncrementNumRowsReturned(num_rows_added); |
| if (*eos && debug_dump_text_writer_) { |
| DCHECK(debug_dump_text_writer_ref_ != nullptr); |
| TupleFileReader cache_reader( |
| ExecEnv::GetInstance()->tuple_cache_mgr()->GetPath(handle_), mem_tracker(), |
| runtime_profile()); |
| RETURN_IF_ERROR(cache_reader.Open(state)); |
| // Read the cache entries from the cache reader, and write as the reference |
| // debug cache file. If an error occurs, abort the verification, and return the |
| // error status. |
| RowBatch row_batch(child(0)->row_desc(), state->batch_size(), mem_tracker()); |
| bool cache_eos = false; |
| while (!cache_eos) { |
| RETURN_IF_ERROR( |
| cache_reader.GetNext(state, buffer_pool_client(), &row_batch, &cache_eos)); |
| DCHECK(row_batch.num_rows() > 0 || cache_eos); |
| RETURN_IF_ERROR(debug_dump_text_writer_ref_->Write(&row_batch)); |
| row_batch.Reset(); |
| } |
| DCHECK(cache_eos); |
| debug_dump_text_writer_ref_->Commit(); |
| RETURN_IF_ERROR(VerifyAndMoveDebugCache(state)); |
| } |
| COUNTER_SET(rows_returned_counter_, rows_returned()); |
| return Status::OK(); |
| } |
| |
| void TupleCacheNode::ReleaseResult() { |
| reader_.reset(); |
| writer_.reset(); |
| handle_.reset(); |
| } |
| |
| Status TupleCacheNode::Reset(RuntimeState* state, RowBatch* row_batch) { |
| // Reset() is not supported. |
| DCHECK(false) << "Internal error: Tuple cache nodes should not appear in subplans."; |
| return Status("Internal error: Tuple cache nodes should not appear in subplans."); |
| } |
| |
| void TupleCacheNode::Close(RuntimeState* state) { |
| if (is_closed()) return; |
| // If we reach this point with an open writer_, then this cache entry is invalid. We |
| // will delete the file and abort the write. This can happen if the query is cancelled, |
| // if the query hits an error, or if a parent node has a limit and doesn't complete |
| // fetching. This is intentionally restrictive. |
| if (writer_) { |
| TupleCacheMgr* tuple_cache_mgr = ExecEnv::GetInstance()->tuple_cache_mgr(); |
| writer_->Abort(); |
| tuple_cache_mgr->AbortWrite(move(handle_), false); |
| } |
| if (debug_dump_text_writer_) { |
| debug_dump_text_writer_->Delete(); |
| debug_dump_text_writer_.reset(); |
| } |
| if (debug_dump_text_writer_ref_) { |
| debug_dump_text_writer_ref_->Delete(); |
| debug_dump_text_writer_ref_.reset(); |
| } |
| ReleaseResult(); |
| ExecNode::Close(state); |
| } |
| |
| string TupleCacheNode::GetDebugDumpPath(const RuntimeState* state, |
| const string& org_fragment_id, string* sub_dir_full_path) const { |
| // The name of the subdirectory is hash key. |
| // For non-reference files, the file name is the fragment instance node id. |
| // For reference files, the name includes the current fragment instance node id, the |
| // original fragment node id, and a "ref" suffix. |
| string file_name = GenerateFragmentNodeId(state, this); |
| if (!org_fragment_id.empty()) { |
| // Adds the original fragment id of the cache to the path for debugging purpose. |
| file_name = Substitute("$0_$1_ref", file_name, org_fragment_id); |
| } |
| return ExecEnv::GetInstance()->tuple_cache_mgr()->GetDebugDumpPath( |
| combined_key_, file_name, sub_dir_full_path); |
| } |
| |
| void TupleCacheNode::DebugString(int indentation_level, stringstream* out) const { |
| *out << string(indentation_level * 2, ' '); |
| *out << "TupleCacheNode(" << combined_key_; |
| ExecNode::DebugString(indentation_level, out); |
| *out << ")"; |
| } |
| |
| // This hashes all the fields of the HdfsPartitionDescriptor, except: |
| // 1. block_size: Only used for writing, so it doesn't matter for reading |
| // 2. location: The location is hashed as part of the scan range |
| // 3. id: The partition id is not stable over time |
| // As a substitute for the "id", this uses the partition key expr hash, which |
| // is a stable identifier for the partition. |
| uint32_t TupleCacheNode::HashHdfsPartitionDescriptor( |
| const HdfsPartitionDescriptor* partition_desc, uint32_t seed) { |
| uint32_t hash = seed; |
| char line_delim = partition_desc->line_delim(); |
| hash = HashUtil::Hash(&line_delim, sizeof(line_delim), hash); |
| char field_delim = partition_desc->field_delim(); |
| hash = HashUtil::Hash(&field_delim, sizeof(field_delim), hash); |
| char collection_delim = partition_desc->collection_delim(); |
| hash = HashUtil::Hash(&collection_delim, sizeof(collection_delim), hash); |
| char escape_char = partition_desc->escape_char(); |
| hash = HashUtil::Hash(&escape_char, sizeof(escape_char), hash); |
| std::string file_format = to_string(partition_desc->file_format()); |
| hash = HashUtil::Hash(file_format.data(), file_format.length(), hash); |
| hash = HashUtil::Hash(partition_desc->encoding_value().data(), |
| partition_desc->encoding_value().length(), hash); |
| std::string json_binary_format = to_string(partition_desc->json_binary_format()); |
| hash = HashUtil::Hash(json_binary_format.data(), json_binary_format.length(), hash); |
| uint32_t partition_key_expr_hash = partition_desc->partition_key_expr_hash(); |
| hash = HashUtil::Hash(&partition_key_expr_hash, sizeof(partition_key_expr_hash), hash); |
| return hash; |
| } |
| |
| uint32_t TupleCacheNode::HashHdfsFileSplit(const HdfsFileSplitPB& split, uint32_t seed) { |
| uint32_t hash = seed; |
| if (split.has_relative_path() && !split.relative_path().empty()) { |
| hash = HashUtil::Hash( |
| split.relative_path().data(), split.relative_path().length(), hash); |
| DCHECK(split.has_partition_path_hash()); |
| int32_t partition_path_hash = split.partition_path_hash(); |
| hash = HashUtil::Hash(&partition_path_hash, sizeof(partition_path_hash), hash); |
| } else if (split.has_absolute_path() && !split.absolute_path().empty()) { |
| hash = HashUtil::Hash( |
| split.absolute_path().data(), split.absolute_path().length(), hash); |
| } else { |
| DCHECK("Either relative_path or absolute_path must be set"); |
| } |
| DCHECK(split.has_offset()); |
| int64_t offset = split.offset(); |
| hash = HashUtil::Hash(&offset, sizeof(offset), hash); |
| DCHECK(split.has_length()); |
| int64_t length = split.length(); |
| hash = HashUtil::Hash(&length, sizeof(length), hash); |
| DCHECK(split.has_mtime()); |
| int64_t mtime = split.mtime(); |
| hash = HashUtil::Hash(&mtime, sizeof(mtime), hash); |
| return hash; |
| } |
| |
| void TupleCacheNode::ComputeFragmentInstanceKey(const RuntimeState* state) { |
| const PlanFragmentInstanceCtxPB& ctx = state->instance_ctx_pb(); |
| uint32_t hash = 0; |
| // Collect the HdfsScanNodes below this point. The HdfsScanNodes have information about |
| // the partitions that we need to include in the fragment instance key. Some locations |
| // may have a large number of scan nodes below them, so construct a map from the node |
| // id to the HdfsScanNodeBase. |
| vector<ExecNode*> scan_nodes; |
| CollectNodes(TPlanNodeType::HDFS_SCAN_NODE, &scan_nodes); |
| unordered_map<int, const HdfsScanNodeBase*> id_to_scan_node_map; |
| for (const ExecNode* exec_node : scan_nodes) { |
| const HdfsScanNodeBase* scan_node = |
| static_cast<const HdfsScanNodeBase*>(exec_node); |
| int node_id = exec_node->plan_node().tnode_->node_id; |
| DCHECK(id_to_scan_node_map.find(node_id) == id_to_scan_node_map.end()) |
| << "Duplicate scan node id: " << node_id; |
| id_to_scan_node_map[node_id] = scan_node; |
| } |
| for (int32_t node_id : plan_node().tnode_->tuple_cache_node.input_scan_node_ids) { |
| const HdfsTableDescriptor* hdfs_table = id_to_scan_node_map[node_id]->hdfs_table(); |
| DCHECK(hdfs_table != nullptr); |
| auto ranges = ctx.per_node_scan_ranges().find(node_id); |
| if (ranges == ctx.per_node_scan_ranges().end()) continue; |
| for (const ScanRangeParamsPB& params : ranges->second.scan_ranges()) { |
| // This only supports HDFS right now |
| DCHECK(params.scan_range().has_hdfs_file_split()); |
| const HdfsFileSplitPB& split = params.scan_range().hdfs_file_split(); |
| // Information on the partition can influence how files are processed. For example, |
| // for text files, the delimiter can be specified on the partition level. There |
| // are several such attributes. We need to incorporate the partition information |
| // into the hash for each file split. |
| const HdfsPartitionDescriptor* partition_desc = |
| hdfs_table->GetPartition(split.partition_id()); |
| DCHECK(partition_desc != nullptr); |
| if (partition_desc != nullptr) { |
| hash = HashHdfsPartitionDescriptor(partition_desc, hash); |
| } else { |
| LOG(WARNING) << "Partition id " << split.partition_id() |
| << " not found in table " << hdfs_table->fully_qualified_name() |
| << " split filename: " << split.relative_path(); |
| } |
| hash = HashHdfsFileSplit(split, hash); |
| } |
| } |
| fragment_instance_key_ = hash; |
| } |
| |
| } |