Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cpp/src/parquet/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,14 @@ add_parquet_test(arrow-reader-writer-test
arrow/arrow_statistics_test.cc
arrow/variant_test.cc)

if(ARROW_WITH_OPENTELEMETRY)
add_parquet_test(arrow-reader-writer-tracing-test
SOURCES
arrow/arrow_reader_writer_tracing_test.cc
EXTRA_LINK_LIBS
${ARROW_OPENTELEMETRY_LIBS})
endif()

add_parquet_test(arrow-index-test SOURCES arrow/index_test.cc)

add_parquet_test(arrow-internals-test SOURCES arrow/path_internal_test.cc
Expand Down
272 changes: 272 additions & 0 deletions cpp/src/parquet/arrow/arrow_reader_writer_tracing_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
// 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 <chrono>
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

#include <gtest/gtest.h>

#include <opentelemetry/nostd/variant.h>
#include <opentelemetry/sdk/common/attribute_utils.h>
#include <opentelemetry/sdk/common/exporter_utils.h>
#include <opentelemetry/sdk/trace/exporter.h>
#include <opentelemetry/sdk/trace/simple_processor.h>
#include <opentelemetry/sdk/trace/span_data.h>
#include <opentelemetry/sdk/trace/tracer_provider.h>
#include <opentelemetry/trace/provider.h>

#include "arrow/api.h"
#include "arrow/io/api.h"
#include "arrow/testing/gtest_util.h"
#include "arrow/testing/util.h"
#include "parquet/arrow/reader.h"
#include "parquet/arrow/writer.h"
#include "parquet/file_reader.h"

namespace parquet {
namespace arrow {

namespace {

namespace otel = opentelemetry;
namespace sdktrace = opentelemetry::sdk::trace;

struct CapturedSpan {
std::string name;
std::unordered_map<std::string, otel::sdk::common::OwnedAttributeValue> attributes;
};

class CapturedSpanStorage {
public:
void Clear() {
std::lock_guard<std::mutex> lock(mutex_);
spans_.clear();
}

void Append(const sdktrace::SpanData& span) {
std::lock_guard<std::mutex> lock(mutex_);
auto name = span.GetName();
spans_.push_back({std::string(name.data(), name.size()), span.GetAttributes()});
}

std::vector<CapturedSpan> ReadColumnSpans() const {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<CapturedSpan> result;
for (const auto& span : spans_) {
if (span.name == "parquet::arrow::read_column") {
result.push_back(span);
}
}
return result;
}

private:
mutable std::mutex mutex_;
std::vector<CapturedSpan> spans_;
};

class CapturingSpanExporter : public sdktrace::SpanExporter {
public:
explicit CapturingSpanExporter(std::shared_ptr<CapturedSpanStorage> storage)
: storage_(std::move(storage)) {}

std::unique_ptr<sdktrace::Recordable> MakeRecordable() noexcept override {
return std::make_unique<sdktrace::SpanData>();
}

otel::sdk::common::ExportResult Export(
const otel::nostd::span<std::unique_ptr<sdktrace::Recordable>>& spans) noexcept
override {
try {
for (const auto& recordable : spans) {
auto* span = dynamic_cast<sdktrace::SpanData*>(recordable.get());
if (span == nullptr) {
return otel::sdk::common::ExportResult::kFailure;
}
storage_->Append(*span);
}
return otel::sdk::common::ExportResult::kSuccess;
} catch (...) {
return otel::sdk::common::ExportResult::kFailure;
}
}

bool ForceFlush(std::chrono::microseconds) noexcept override { return true; }

bool Shutdown(std::chrono::microseconds) noexcept override { return true; }

private:
std::shared_ptr<CapturedSpanStorage> storage_;
};

const auto kSpanStorage = std::make_shared<CapturedSpanStorage>();

class OtelEnvironment : public ::testing::Environment {
public:
void SetUp() override {
auto exporter = std::make_unique<CapturingSpanExporter>(kSpanStorage);
auto processor = std::make_unique<sdktrace::SimpleSpanProcessor>(std::move(exporter));
auto provider = otel::nostd::shared_ptr<sdktrace::TracerProvider>(
new sdktrace::TracerProvider(std::move(processor)));
otel::trace::Provider::SetTracerProvider(std::move(provider));
}
};

[[maybe_unused]] static ::testing::Environment* kOtelEnvironment =
::testing::AddGlobalTestEnvironment(new OtelEnvironment);

::arrow::Result<std::shared_ptr<::arrow::Buffer>> WriteToBuffer(
const std::shared_ptr<::arrow::Table>& table) {
ARROW_ASSIGN_OR_RAISE(auto sink, ::arrow::io::BufferOutputStream::Create());
RETURN_NOT_OK(
WriteTable(*table, ::arrow::default_memory_pool(), sink, table->num_rows()));
return sink->Finish();
}

::arrow::Result<std::unique_ptr<FileReader>> OpenReader(
const std::shared_ptr<::arrow::Table>& table) {
ARROW_ASSIGN_OR_RAISE(auto buffer, WriteToBuffer(table));
auto parquet_reader =
ParquetFileReader::Open(std::make_shared<::arrow::io::BufferReader>(buffer));
ARROW_ASSIGN_OR_RAISE(auto reader, FileReader::Make(::arrow::default_memory_pool(),
std::move(parquet_reader)));
reader->set_use_threads(false);
return reader;
}

std::shared_ptr<::arrow::Table> NestedTable() {
auto table_schema = ::arrow::schema(
{::arrow::field("first", ::arrow::int32()),
::arrow::field("group",
::arrow::struct_({::arrow::field("left", ::arrow::int32()),
::arrow::field("right", ::arrow::utf8())})),
::arrow::field("last", ::arrow::int64())});
return ::arrow::TableFromJSON(table_schema,
{R"([{"first": 1, "group": {"left": 10, "right": "a"},
"last": 100},
{"first": 2, "group": {"left": 20, "right": "b"},
"last": 200}])"});
}

template <typename T>
const T* GetAttribute(const CapturedSpan& span, const std::string& name) {
auto it = span.attributes.find(name);
if (it == span.attributes.end()) {
return nullptr;
}
return otel::nostd::get_if<T>(&it->second);
}

void AssertColumnAttributes(const CapturedSpan& span, int32_t field_index,
const std::string& field_name,
const std::string& physical_type) {
const auto* actual_index = GetAttribute<int32_t>(span, "parquet.arrow.columnindex");
ASSERT_NE(actual_index, nullptr);
EXPECT_EQ(*actual_index, field_index);

const auto* actual_name = GetAttribute<std::string>(span, "parquet.arrow.columnname");
ASSERT_NE(actual_name, nullptr);
EXPECT_EQ(*actual_name, field_name);

const auto* actual_type = GetAttribute<std::string>(span, "parquet.arrow.physicaltype");
ASSERT_NE(actual_type, nullptr);
EXPECT_EQ(*actual_type, physical_type);
}

TEST(ReadColumnTracing, FlatColumnSubset) {
auto table_schema = ::arrow::schema({::arrow::field("first", ::arrow::int32()),
::arrow::field("target", ::arrow::utf8())});
auto table = ::arrow::TableFromJSON(
table_schema, {R"([{"first": 1, "target": "a"}, {"first": 2, "target": "b"}])"});
ASSERT_OK_AND_ASSIGN(auto reader, OpenReader(table));

kSpanStorage->Clear();
ASSERT_OK_AND_ASSIGN(auto result, reader->ReadTable({1}));
ASSERT_EQ(result->num_columns(), 1);
ASSERT_TRUE(result->column(0)->Equals(table->column(1)));

auto spans = kSpanStorage->ReadColumnSpans();
ASSERT_EQ(spans.size(), 1);
AssertColumnAttributes(spans[0], 1, "target", "BYTE_ARRAY");
}

TEST(ReadColumnTracing, NestedColumnSubset) {
ASSERT_OK_AND_ASSIGN(auto reader, OpenReader(NestedTable()));

kSpanStorage->Clear();
ASSERT_OK_AND_ASSIGN(auto result, reader->ReadTable({2, 3}));
ASSERT_EQ(result->num_columns(), 2);
ASSERT_EQ(result->schema()->field(0)->name(), "group");
ASSERT_EQ(result->schema()->field(1)->name(), "last");

auto spans = kSpanStorage->ReadColumnSpans();
ASSERT_EQ(spans.size(), 2);
AssertColumnAttributes(spans[0], 1, "group", "");
AssertColumnAttributes(spans[1], 2, "last", "INT64");
}

TEST(ReadColumnTracing, ReorderedColumnSubset) {
ASSERT_OK_AND_ASSIGN(auto reader, OpenReader(NestedTable()));

kSpanStorage->Clear();
ASSERT_OK_AND_ASSIGN(auto result, reader->ReadTable({3, 0, 2}));
ASSERT_EQ(result->num_columns(), 3);

auto spans = kSpanStorage->ReadColumnSpans();
ASSERT_EQ(spans.size(), 3);
AssertColumnAttributes(spans[0], 2, "last", "INT64");
AssertColumnAttributes(spans[1], 0, "first", "INT32");
AssertColumnAttributes(spans[2], 1, "group", "");
}

TEST(ReadColumnTracing, FullNestedSchema) {
ASSERT_OK_AND_ASSIGN(auto reader, OpenReader(NestedTable()));

kSpanStorage->Clear();
ASSERT_OK_AND_ASSIGN(auto result, reader->ReadTable());
ASSERT_EQ(result->num_columns(), 3);

auto spans = kSpanStorage->ReadColumnSpans();
ASSERT_EQ(spans.size(), 3);
AssertColumnAttributes(spans[0], 0, "first", "INT32");
AssertColumnAttributes(spans[1], 1, "group", "");
AssertColumnAttributes(spans[2], 2, "last", "INT64");
}

TEST(ReadColumnTracing, DirectNestedLeafRead) {
ASSERT_OK_AND_ASSIGN(auto reader, OpenReader(NestedTable()));

kSpanStorage->Clear();
std::shared_ptr<::arrow::ChunkedArray> result;
ASSERT_OK(reader->RowGroup(0)->Column(2)->Read(&result));
ASSERT_EQ(result->length(), 2);

auto spans = kSpanStorage->ReadColumnSpans();
ASSERT_EQ(spans.size(), 1);
AssertColumnAttributes(spans[0], 1, "group", "");
}

} // namespace

} // namespace arrow
} // namespace parquet
38 changes: 24 additions & 14 deletions cpp/src/parquet/arrow/reader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,8 @@ class FileReaderImpl : public FileReader {
reader_->metadata()->key_value_metadata(), out);
}

Status ReadColumn(int i, const std::vector<int>& row_groups, ColumnReader* reader,
std::shared_ptr<ChunkedArray>* out) {
Status ReadColumn(int field_index, const std::vector<int>& row_groups,
ColumnReader* reader, std::shared_ptr<ChunkedArray>* out) {
BEGIN_PARQUET_CATCH_EXCEPTIONS
// NextBatch()'s size is a number of records (rows), not leaf values, so use the
// row group's own row count directly rather than some column's num_values().
Expand All @@ -278,12 +278,18 @@ class FileReaderImpl : public FileReader {
records_to_read += reader_->metadata()->RowGroup(row_group)->num_rows();
}
#ifdef ARROW_WITH_OPENTELEMETRY
std::string column_name = reader_->metadata()->schema()->Column(i)->name();
std::string phys_type =
TypeToString(reader_->metadata()->schema()->Column(i)->physical_type());
const auto& schema_field = manifest_.schema_fields[field_index];
const std::string& column_name = schema_field.field->name();
std::string phys_type;
if (schema_field.is_leaf()) {
phys_type = TypeToString(reader_->metadata()
->schema()
->Column(schema_field.column_index)
->physical_type());
}
::arrow::util::tracing::Span span;
START_SPAN(span, "parquet::arrow::read_column",
{{"parquet.arrow.columnindex", i},
{{"parquet.arrow.columnindex", field_index},
{"parquet.arrow.columnname", column_name},
{"parquet.arrow.physicaltype", phys_type},
{"parquet.arrow.records_to_read", records_to_read}});
Expand All @@ -292,15 +298,17 @@ class FileReaderImpl : public FileReader {
END_PARQUET_CATCH_EXCEPTIONS
}

Status ReadColumn(int i, const std::vector<int>& row_groups,
Status ReadColumn(int column_index, const std::vector<int>& row_groups,
std::shared_ptr<ChunkedArray>* out) {
std::unique_ptr<ColumnReader> flat_column_reader;
RETURN_NOT_OK(GetColumn(i, SomeRowGroupsFactory(row_groups), &flat_column_reader));
return ReadColumn(i, row_groups, flat_column_reader.get(), out);
RETURN_NOT_OK(
GetColumn(column_index, SomeRowGroupsFactory(row_groups), &flat_column_reader));
ARROW_ASSIGN_OR_RAISE(auto field_indices, manifest_.GetFieldIndices({column_index}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this change is correct. See the documentation for FileReader::ReadColumn here.

In this method, i (or now column_index) is already a top-level field index. So there's no need to convert to a a field index using manifest_.GetFieldIndices.

For clarity, it would make sense to rename i to field_index in this method and the one below (and in the header too).

return ReadColumn(field_indices.front(), row_groups, flat_column_reader.get(), out);
}

Status ReadColumn(int i, std::shared_ptr<ChunkedArray>* out) override {
return ReadColumn(i, Iota(reader_->metadata()->num_row_groups()), out);
Status ReadColumn(int column_index, std::shared_ptr<ChunkedArray>* out) override {
return ReadColumn(column_index, Iota(reader_->metadata()->num_row_groups()), out);
}

Result<std::shared_ptr<Table>> ReadTable() override {
Expand Down Expand Up @@ -1374,14 +1382,16 @@ Future<std::shared_ptr<Table>> FileReaderImpl::DecodeRowGroups(
std::vector<std::shared_ptr<ColumnReaderImpl>> readers;
std::shared_ptr<::arrow::Schema> result_schema;
RETURN_NOT_OK(GetFieldReaders(column_indices, row_groups, &readers, &result_schema));
ARROW_ASSIGN_OR_RAISE(auto field_indices, manifest_.GetFieldIndices(column_indices));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change being correct relies on an assumption that the order of field indices returned by manifest_.GetFieldIndices is the same as the order the readers are in. This is true, because of how GetFieldReaders is implemented, but a bit fragile and could be broken by a future code change.

Can you instead refactor GetFieldReaders so it also returns the field indices? It's a method on the internal FileReaderImpl class so it's OK to change the method signature.

This would also avoid the overhead of creating the field indices vector twice.

// OptionalParallelForAsync requires an executor
if (!cpu_executor) cpu_executor = ::arrow::internal::GetCpuThreadPool();

auto read_column = [row_groups, self, this](size_t i,
std::shared_ptr<ColumnReaderImpl> reader)
auto read_column = [field_indices, row_groups, self, this](
size_t reader_index, std::shared_ptr<ColumnReaderImpl> reader)
-> ::arrow::Result<std::shared_ptr<::arrow::ChunkedArray>> {
std::shared_ptr<::arrow::ChunkedArray> column;
RETURN_NOT_OK(ReadColumn(static_cast<int>(i), row_groups, reader.get(), &column));
RETURN_NOT_OK(
ReadColumn(field_indices[reader_index], row_groups, reader.get(), &column));
return column;
};
auto make_table = [result_schema, row_groups, self,
Expand Down
Loading