| // 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 "exec/tuple-file-reader.h" |
| |
| #include "gutil/strings/substitute.h" |
| #include "kudu/util/array_view.h" |
| #include "kudu/util/env.h" |
| #include "kudu/util/slice.h" |
| #include "runtime/mem-tracker.h" |
| #include "runtime/row-batch.h" |
| #include "runtime/runtime-state.h" |
| #include "util/debug-util.h" |
| #include "util/kudu-status-util.h" |
| |
| #include "common/names.h" |
| |
| namespace impala { |
| |
| TupleFileReader::TupleFileReader( |
| const std::string& path, MemTracker* parent, RuntimeProfile* profile) |
| : path_(path), |
| tracker_(new MemTracker(-1, "TupleFileReader", parent)), |
| read_timer_(profile ? ADD_TIMER(profile, "TupleCacheReadTime") : nullptr), |
| deserialize_timer_( |
| profile ? ADD_TIMER(profile, "TupleCacheDeserializeTime") : nullptr), |
| bytes_read_(profile ? |
| ADD_COUNTER(profile, "TupleCacheBytesRead", TUnit::BYTES) : nullptr) {} |
| |
| TupleFileReader::~TupleFileReader() { |
| // MemTracker expects an explicit close. Since the TupleFileReader can be freed |
| // before query execution ends, this must unregister the MemTracker from its parent |
| // before it is freed. |
| tracker_->CloseAndUnregisterFromParent(); |
| } |
| |
| Status TupleFileReader::Open(RuntimeState *state) { |
| kudu::RWFileOptions opts; |
| opts.mode = kudu::Env::OpenMode::MUST_EXIST; |
| KUDU_RETURN_IF_ERROR( |
| kudu::Env::Default()->NewRWFile(opts, path_, &reader_), |
| "Failed to open tuple cache file"); |
| // Get the file size |
| KUDU_RETURN_IF_ERROR(reader_->Size(&file_size_), |
| "Failed to get size for the tuple cache file"); |
| RETURN_IF_ERROR(DebugAction(state->query_options(), "TUPLE_FILE_READER_OPEN")); |
| return Status::OK(); |
| } |
| |
| Status TupleFileReader::GetNext(RuntimeState *state, |
| BufferPool::ClientHandle* bpclient, RowBatch* output_row_batch, bool* eos) { |
| if (offset_ == file_size_) { |
| *eos = true; |
| return Status::OK(); |
| } |
| // Only the first rowbatch starts at the zero offset of the file. |
| // We use this for injecting errors for testing purposes. |
| bool first_rowbatch = offset_ == 0; |
| *eos = false; |
| SCOPED_TIMER(read_timer_); |
| // Each block starts with the sizes of the variable chunks of data: |
| // 1. The header size |
| // 2. The tuple data size |
| // 3. The tuple offsets size |
| size_t header_len; |
| size_t tuple_data_len; |
| size_t tuple_offsets_len; |
| |
| vector<kudu::Slice> chunk_lens_slices = { |
| kudu::Slice(reinterpret_cast<const char*>(&header_len), sizeof(header_len)), |
| kudu::Slice(reinterpret_cast<const char*>(&tuple_data_len), |
| sizeof(tuple_data_len)), |
| kudu::Slice(reinterpret_cast<const char*>(&tuple_offsets_len), |
| sizeof(tuple_offsets_len))}; |
| |
| KUDU_RETURN_IF_ERROR(reader_->ReadV(offset_, |
| kudu::ArrayView<kudu::Slice>(chunk_lens_slices)), |
| "Failed to read cache file"); |
| size_t chunk_lens_slices_size = sizeof(header_len) + sizeof(tuple_data_len) + |
| sizeof(tuple_offsets_len); |
| COUNTER_ADD(bytes_read_, chunk_lens_slices_size); |
| |
| // tuple_data_len can be zero, see IMPALA-13411. |
| if (header_len == 0 || tuple_offsets_len == 0) { |
| string err_msg = Substitute("Invalid data lengths at offset $0 in $1: " |
| "header_len=$2, tuple_data_len=$3, tuple_offsets_len=$4", offset_, path_, |
| header_len, tuple_data_len, tuple_offsets_len); |
| DCHECK(false) << err_msg; |
| return Status(Substitute("Invalid tuple cache file: $0", err_msg)); |
| } |
| offset_ += chunk_lens_slices_size; |
| |
| // Now, we know the total size of the variable-length data, and we can read |
| // it in a single chunk. |
| size_t varlen_size = header_len + tuple_data_len + tuple_offsets_len; |
| |
| // Sanity check: The varlen_size shouldn't be larger than the rest of the file. |
| // This protects us from doing very large memory allocations if any of the lengths |
| // are bogus. |
| if (offset_ + varlen_size > file_size_) { |
| string err_msg = Substitute("Invalid data lengths at offset $0 in $1 exceed " |
| "file size $2: header_len=$3, tuple_data_len=$4, tuple_offsets_len=$5", |
| offset_, path_, file_size_, header_len, tuple_data_len, tuple_offsets_len); |
| DCHECK(false) << err_msg; |
| return Status(Substitute("Invalid tuple cache file: $0", err_msg)); |
| } |
| std::unique_ptr<char []> varlen_data(new char[varlen_size]); |
| |
| kudu::Slice header_slice = kudu::Slice(varlen_data.get(), header_len); |
| kudu::Slice tuple_data_slice = |
| kudu::Slice(varlen_data.get() + header_len, tuple_data_len); |
| kudu::Slice tuple_offsets_slice = |
| kudu::Slice(varlen_data.get() + header_len + tuple_data_len, tuple_offsets_len); |
| std::vector<kudu::Slice> varlen_data_slices = |
| { header_slice, tuple_data_slice, tuple_offsets_slice }; |
| |
| KUDU_RETURN_IF_ERROR(reader_->ReadV(offset_, |
| kudu::ArrayView<kudu::Slice>(varlen_data_slices)), |
| "Failed to read tuple cache file"); |
| COUNTER_ADD(bytes_read_, varlen_size); |
| offset_ += varlen_size; |
| |
| RowBatchHeaderPB header; |
| // The header is at the start of the varlen data |
| if (!header.ParseFromArray(header_slice.data(), header_slice.size())) { |
| return Status(TErrorCode::INTERNAL_ERROR, |
| "Could not deserialize RowBatchHeaderPB from disk cache"); |
| } |
| |
| std::unique_ptr<RowBatch> row_batch; |
| { |
| SCOPED_TIMER(deserialize_timer_); |
| RETURN_IF_ERROR(RowBatch::FromProtobuf(output_row_batch->row_desc(), header, |
| tuple_offsets_slice, tuple_data_slice, tracker_.get(), bpclient, &row_batch)); |
| } |
| |
| DCHECK_EQ(output_row_batch->num_rows(), 0); |
| if (output_row_batch->capacity() < row_batch->num_rows()) { |
| string err_msg = |
| Substitute("Too many rows ($0) for the output row batch (capacity $1)", |
| row_batch->num_rows(), output_row_batch->capacity()); |
| DCHECK(false) << err_msg; |
| return Status(Substitute("Invalid tuple cache file: $0", err_msg)); |
| } |
| |
| // For testing, we inject an error as late as possible in this function. Nothing |
| // after this point returns not-OK status. If we can recover from this point, |
| // we can recover from previous points. There are two different cases: either |
| // we want an error on the first row batch produced or we want an error on |
| // a subsequent row batch. |
| if (first_rowbatch) { |
| RETURN_IF_ERROR( |
| DebugAction(state->query_options(), "TUPLE_FILE_READER_FIRST_GETNEXT")); |
| } else { |
| RETURN_IF_ERROR( |
| DebugAction(state->query_options(), "TUPLE_FILE_READER_SECOND_GETNEXT")); |
| } |
| // Set eos after any possibility of a not-OK status |
| if (offset_ == file_size_) { |
| *eos = true; |
| } |
| output_row_batch->AddRows(row_batch->num_rows()); |
| for (int row = 0; row < row_batch->num_rows(); row++) { |
| TupleRow* src = row_batch->GetRow(row); |
| TupleRow* dest = output_row_batch->GetRow(row); |
| // if the input row is shorter than the output row, make sure not to leave |
| // uninitialized Tuple* around |
| output_row_batch->ClearRow(dest); |
| // this works as expected if rows from input_batch form a prefix of |
| // rows in output_batch |
| row_batch->CopyRow(src, dest); |
| } |
| output_row_batch->CommitRows(row_batch->num_rows()); |
| row_batch->TransferResourceOwnership(output_row_batch); |
| |
| return Status::OK(); |
| } |
| |
| } // namespace impala |