blob: 9e8076612f1f00e13e6d2f77fd8474bd03a720a8 [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.
*/
#include <gtest/gtest.h>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iterator>
#include <memory>
#include <string>
#include <tuple>
#include <vector>
#include "common/allocator/alloc_base.h"
#include "common/config/config.h"
#include "common/tablet.h"
#include "cwrapper/tsfile_cwrapper.h"
#include "file/tsfile_io_reader.h"
#include "file/write_file.h"
#include "reader/filter/tag_filter.h"
#include "reader/meta_data_querier.h"
#include "reader/table_result_set.h"
#include "reader/tsfile_reader.h"
#include "utils/injection.h"
#include "writer/tsfile_table_writer.h"
namespace {
class InjectionGuard {
public:
explicit InjectionGuard(const char* point, int count_down = 0)
: point_(point) {
common::enable_injection(point_, count_down);
}
~InjectionGuard() { common::disable_injection(point_); }
private:
const char* point_;
};
class FailingReadFile : public storage::RandomAccessReadFile {
public:
explicit FailingReadFile(const std::vector<char>& bytes) : bytes_(bytes) {}
bool is_opened() const override { return opened_; }
int64_t file_size() const override { return bytes_.size(); }
const std::string& file_path() const override { return path_; }
int generation(uint64_t& size, uint64_t& fingerprint) const override {
size = bytes_.size();
fingerprint = 0;
return common::E_OK;
}
int read(int64_t offset, char* buffer, int32_t size,
int32_t& read_size) override {
++reads;
read_size = 0;
if (fail_at > 0 && (persistent ? reads >= fail_at : reads == fail_at)) {
failed = true;
return short_read ? common::E_OK : common::E_FILE_READ_ERR;
}
if (offset < 0 || size < 0) return common::E_INVALID_ARG;
if (offset >= static_cast<int64_t>(bytes_.size())) return common::E_OK;
read_size = static_cast<int32_t>(
std::min<int64_t>(size, bytes_.size() - offset));
std::memcpy(buffer, bytes_.data() + offset, read_size);
return common::E_OK;
}
void close() override { opened_ = false; }
int reads = 0;
int fail_at = 0;
bool persistent = false;
bool short_read = false;
bool failed = false;
private:
const std::vector<char>& bytes_;
bool opened_ = true;
std::string path_ = "memory://table-read-failure";
};
// One tag exercises TagEq's direct lookup; two tags exercise filtered
// traversal. Two devices fit in a leaf; five force multiple levels of internal
// index nodes.
class TableReadFailureTest
: public ::testing::TestWithParam<std::tuple<int, int>> {
protected:
void SetUp() override {
storage::libtsfile_init();
saved_index_degree_ = common::g_config_value_.max_degree_of_index_node_;
ASSERT_EQ(storage::set_max_degree_of_index_node(2), common::E_OK);
filename_ =
"table_read_failure_" +
std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count()) +
".tsfile";
storage::WriteFile file;
int flags = O_WRONLY | O_CREAT | O_TRUNC;
#ifdef _WIN32
flags |= O_BINARY;
#endif
ASSERT_EQ(file.create(filename_, flags, 0666), common::E_OK);
std::vector<common::ColumnSchema> columns;
for (int i = 0; i < std::get<0>(GetParam()); ++i) {
columns.emplace_back("id" + std::to_string(i), common::STRING,
common::UNCOMPRESSED, common::PLAIN,
common::ColumnCategory::TAG);
}
columns.emplace_back("value", common::INT64, common::UNCOMPRESSED,
common::PLAIN, common::ColumnCategory::FIELD);
storage::TableSchema schema("test", columns);
storage::TsFileTableWriter writer(&file, &schema);
storage::Tablet tablet(
"test", schema.get_measurement_names(), schema.get_data_types(),
schema.get_column_categories(), std::get<1>(GetParam()) * 30);
for (int device = 0; device < std::get<1>(GetParam()); ++device) {
for (int t = 0; t < 30; ++t) {
const int row = device * 30 + t;
const std::string id = "d" + std::to_string(device);
ASSERT_EQ(tablet.add_timestamp(row, t), common::E_OK);
ASSERT_EQ(tablet.add_value(row, "id0", id.c_str()),
common::E_OK);
if (std::get<0>(GetParam()) == 2) {
ASSERT_EQ(tablet.add_value(row, "id1", "tag"),
common::E_OK);
}
ASSERT_EQ(
tablet.add_value(row, "value", static_cast<int64_t>(row)),
common::E_OK);
}
}
ASSERT_EQ(writer.write_table(tablet), common::E_OK);
ASSERT_EQ(writer.flush(), common::E_OK);
ASSERT_EQ(writer.close(), common::E_OK);
std::ifstream input(filename_, std::ios::binary);
ASSERT_TRUE(input.is_open());
bytes_.assign(std::istreambuf_iterator<char>(input),
std::istreambuf_iterator<char>());
}
void TearDown() override {
storage::set_max_degree_of_index_node(saved_index_degree_);
std::remove(filename_.c_str());
storage::libtsfile_destroy();
}
struct ScanResult {
int ret = common::E_OK;
int rows = 0;
int open_reads = 0;
int reads = 0;
bool failed = false;
};
void scan(bool filter, int batch_size, int fail_at, bool persistent,
bool short_read, ScanResult& out) {
storage::TsFileReader reader;
auto* source = new FailingReadFile(bytes_);
source->fail_at = fail_at;
source->persistent = persistent;
source->short_read = short_read;
ASSERT_EQ(
reader.open(std::unique_ptr<storage::RandomAccessReadFile>(source)),
common::E_OK);
out.open_reads = source->reads;
storage::TagEq eq(1, "d0");
storage::ResultSet* result = nullptr;
out.ret = reader.query("test", {"id0", "value"}, 0, INT64_MAX, result,
filter ? &eq : nullptr, batch_size);
if (out.ret == common::E_OK) {
ASSERT_NE(result, nullptr);
bool next = false;
while ((out.ret = result->next(next)) == common::E_OK && next) {
if (batch_size == 0) {
++out.rows;
} else {
common::TsBlock* block = nullptr;
out.ret = result->get_next_tsblock(block);
if (out.ret != common::E_OK) break;
ASSERT_NE(block, nullptr);
out.rows += block->get_row_count();
}
}
}
out.reads = source->reads;
out.failed = source->failed;
if (out.ret != common::E_OK && result != nullptr) {
source->fail_at = 0;
bool next = true;
EXPECT_EQ(result->next(next), out.ret);
EXPECT_FALSE(next);
if (batch_size != 0) {
common::TsBlock* block = nullptr;
EXPECT_EQ(result->get_next_tsblock(block), out.ret);
EXPECT_EQ(block, nullptr);
}
EXPECT_EQ(source->reads, out.reads);
}
if (result != nullptr) reader.destroy_query_data_set(result);
}
std::vector<char> bytes_;
std::string filename_;
uint32_t saved_index_degree_ = 0;
};
TEST_P(TableReadFailureTest, EveryReadFailureReachesCaller) {
for (bool filter : {false, true}) {
for (int batch_size : {0, 16}) {
ScanResult baseline;
ASSERT_NO_FATAL_FAILURE(
scan(filter, batch_size, 0, false, false, baseline));
ASSERT_EQ(baseline.ret, common::E_OK);
ASSERT_EQ(baseline.rows,
filter ? 30 : std::get<1>(GetParam()) * 30);
for (bool persistent : {false, true}) {
for (bool short_read : {false, true}) {
for (int fail_at = baseline.open_reads + 1;
fail_at <= baseline.reads; ++fail_at) {
SCOPED_TRACE(::testing::Message()
<< filter << ":" << batch_size << ":"
<< persistent << ":" << short_read << ":"
<< fail_at);
ScanResult failed;
ASSERT_NO_FATAL_FAILURE(scan(filter, batch_size,
fail_at, persistent,
short_read, failed));
EXPECT_TRUE(failed.failed);
EXPECT_EQ(failed.ret, common::E_FILE_READ_ERR);
}
}
}
}
}
}
TEST_P(TableReadFailureTest, MetadataApisPreserveReadErrors) {
for (int operation = 0; operation < 5; ++operation) {
storage::TsFileReader reader;
auto* source = new FailingReadFile(bytes_);
ASSERT_EQ(
reader.open(std::unique_ptr<storage::RandomAccessReadFile>(source)),
common::E_OK);
source->fail_at = source->reads + 1;
source->persistent = true;
TableSchema schema{};
ERRNO schema_error = common::E_OK;
if (operation == 0) {
schema_error = tsfile_reader_get_table_schema_checked(
&reader, "test", &schema);
EXPECT_EQ(schema_error, common::E_FILE_READ_ERR);
EXPECT_EQ(schema.table_name, nullptr);
EXPECT_EQ(schema.column_num, 0);
EXPECT_EQ(schema.column_schemas, nullptr);
} else if (operation == 3) {
uint32_t count = 0;
DeviceSchema* device_schemas = nullptr;
schema_error = tsfile_reader_get_all_timeseries_schemas_checked(
&reader, &device_schemas, &count);
EXPECT_EQ(schema_error, common::E_FILE_READ_ERR);
EXPECT_EQ(count, 0u);
EXPECT_EQ(device_schemas, nullptr);
} else if (operation == 4) {
uint32_t count = 0;
TableSchema* schemas = nullptr;
schema_error = tsfile_reader_get_all_table_schemas_checked(
&reader, &schemas, &count);
EXPECT_EQ(schema_error, common::E_FILE_READ_ERR);
EXPECT_EQ(count, 0u);
EXPECT_EQ(schemas, nullptr);
} else {
ERRNO error = common::E_OK;
TagFilterHandle filter =
operation == 1
? tsfile_tag_filter_create(&reader, "test", "id0", "d0",
TAG_FILTER_EQ, &error)
: tsfile_tag_filter_between(&reader, "test", "id0", "d0",
"d0", false, &error);
EXPECT_EQ(error, common::E_FILE_READ_ERR);
EXPECT_EQ(filter, nullptr);
if (filter != nullptr) tsfile_tag_filter_free(filter);
}
EXPECT_TRUE(source->failed);
// Metadata failures must not poison the reader or cache an empty
// schema.
source->fail_at = 0;
if (operation == 3) {
uint32_t count = 0;
DeviceSchema* device_schemas = nullptr;
schema_error = tsfile_reader_get_all_timeseries_schemas_checked(
&reader, &device_schemas, &count);
ASSERT_EQ(schema_error, common::E_OK);
ASSERT_NE(device_schemas, nullptr);
ASSERT_GT(count, 0u);
for (uint32_t i = 0; i < count; ++i) {
free_device_schema(device_schemas[i]);
}
free(device_schemas);
} else if (operation == 4) {
uint32_t count = 0;
TableSchema* schemas = nullptr;
schema_error = tsfile_reader_get_all_table_schemas_checked(
&reader, &schemas, &count);
ASSERT_EQ(schema_error, common::E_OK);
ASSERT_NE(schemas, nullptr);
ASSERT_GT(count, 0u);
for (uint32_t i = 0; i < count; ++i) {
free_table_schema(schemas[i]);
}
free(schemas);
} else {
schema_error = tsfile_reader_get_table_schema_checked(
&reader, "test", &schema);
ASSERT_EQ(schema_error, common::E_OK);
EXPECT_STREQ(schema.table_name, "test");
free_table_schema(schema);
}
}
}
TEST_P(TableReadFailureTest, CheckedMetadataApisPropagateEveryReadFailure) {
// Enumerating every read also covers failures after device-schema
// entries or device lists have been partially initialized.
for (int operation = 0; operation < 4; ++operation) {
auto invoke = [operation](storage::TsFileReader& reader,
bool expect_failure) {
if (operation == 3) {
std::vector<std::shared_ptr<storage::IDeviceID>> devices(1);
const int ret = reader.get_all_devices("TEST", devices);
if (expect_failure) {
EXPECT_TRUE(devices.empty());
} else {
EXPECT_FALSE(devices.empty());
}
return ret;
}
if (operation == 0) {
auto* schemas = reinterpret_cast<DeviceSchema*>(&reader);
uint32_t count = 7;
const ERRNO ret =
tsfile_reader_get_all_timeseries_schemas_checked(
&reader, &schemas, &count);
if (expect_failure) {
EXPECT_EQ(schemas, nullptr);
EXPECT_EQ(count, 0u);
} else {
EXPECT_NE(schemas, nullptr);
EXPECT_GT(count, 0u);
}
if (schemas != nullptr) {
for (uint32_t i = 0; i < count; ++i) {
free_device_schema(schemas[i]);
}
free(schemas);
}
return ret;
}
TagFilterHandle filter = &reader;
const ERRNO ret =
operation == 1
? tsfile_tag_filter_create_checked(
&reader, "test", "id0", "d0", TAG_FILTER_EQ, &filter)
: tsfile_tag_filter_between_checked(
&reader, "test", "id0", "d0", "d1", false, &filter);
if (expect_failure) {
EXPECT_EQ(filter, nullptr);
} else {
EXPECT_NE(filter, nullptr);
}
tsfile_tag_filter_free(filter);
return ret;
};
int first_read = 0;
int last_read = 0;
{
storage::TsFileReader reader;
auto* source = new FailingReadFile(bytes_);
ASSERT_EQ(
reader.open(
std::unique_ptr<storage::RandomAccessReadFile>(source)),
common::E_OK);
first_read = source->reads + 1;
ASSERT_EQ(invoke(reader, false), common::E_OK);
last_read = source->reads;
ASSERT_GE(last_read, first_read);
}
for (bool short_read : {false, true}) {
for (int fail_at = first_read; fail_at <= last_read; ++fail_at) {
SCOPED_TRACE(::testing::Message()
<< operation << ":" << short_read << ":"
<< fail_at);
const int64_t allocated_before =
common::ModStat::get_instance().get_stat(
common::MOD_TSFILE_READER);
{
storage::TsFileReader reader;
auto* source = new FailingReadFile(bytes_);
ASSERT_EQ(
reader.open(
std::unique_ptr<storage::RandomAccessReadFile>(
source)),
common::E_OK);
source->fail_at = fail_at;
source->short_read = short_read;
EXPECT_EQ(invoke(reader, true), common::E_FILE_READ_ERR);
EXPECT_TRUE(source->failed);
if (operation == 3) {
storage::TsFileReader legacy_reader;
auto* legacy_source = new FailingReadFile(bytes_);
ASSERT_EQ(
legacy_reader.open(
std::unique_ptr<storage::RandomAccessReadFile>(
legacy_source)),
common::E_OK);
legacy_source->fail_at = fail_at;
legacy_source->short_read = short_read;
EXPECT_TRUE(
legacy_reader.get_all_devices("TEST").empty());
EXPECT_TRUE(legacy_source->failed);
source->fail_at = 0;
EXPECT_EQ(invoke(reader, false), common::E_OK);
}
}
EXPECT_EQ(common::ModStat::get_instance().get_stat(
common::MOD_TSFILE_READER),
allocated_before);
}
}
}
}
TEST_P(TableReadFailureTest, TableScopedDeviceEnumerationValidatesInputs) {
storage::TsFileReader reader;
std::vector<std::shared_ptr<storage::IDeviceID>> devices(1);
EXPECT_EQ(reader.get_all_devices("test", devices), common::E_INVALID_ARG);
EXPECT_TRUE(devices.empty());
EXPECT_TRUE(reader.get_all_devices("test").empty());
auto* source = new FailingReadFile(bytes_);
ASSERT_EQ(
reader.open(std::unique_ptr<storage::RandomAccessReadFile>(source)),
common::E_OK);
ASSERT_EQ(reader.get_all_devices("TEST", devices), common::E_OK);
EXPECT_EQ(devices.size(), static_cast<size_t>(std::get<1>(GetParam())));
EXPECT_EQ(reader.get_all_devices("TEST").size(), devices.size());
ASSERT_EQ(reader.get_all_devices("missing", devices), common::E_OK);
EXPECT_TRUE(devices.empty());
}
TEST_P(TableReadFailureTest, LegacySchemaApisReturnEmptyOnReadFailure) {
for (int operation = 0; operation < 4; ++operation) {
SCOPED_TRACE(operation);
storage::TsFileReader reader;
auto* source = new FailingReadFile(bytes_);
ASSERT_EQ(
reader.open(std::unique_ptr<storage::RandomAccessReadFile>(source)),
common::E_OK);
source->fail_at = source->reads + 1;
source->persistent = true;
if (operation == 0) {
TableSchema schema =
tsfile_reader_get_table_schema(&reader, "test");
EXPECT_EQ(schema.table_name, nullptr);
EXPECT_EQ(schema.column_num, 0);
EXPECT_EQ(schema.column_schemas, nullptr);
free_table_schema(schema);
} else if (operation == 1) {
uint32_t count = 7;
TableSchema* schemas =
tsfile_reader_get_all_table_schemas(&reader, &count);
EXPECT_EQ(schemas, nullptr);
EXPECT_EQ(count, 0u);
if (schemas != nullptr) {
for (uint32_t i = 0; i < count; ++i)
free_table_schema(schemas[i]);
free(schemas);
}
} else if (operation == 2) {
uint32_t count = 7;
DeviceSchema* schemas =
tsfile_reader_get_all_timeseries_schemas(&reader, &count);
EXPECT_EQ(schemas, nullptr);
EXPECT_EQ(count, 0u);
if (schemas != nullptr) {
for (uint32_t i = 0; i < count; ++i)
free_device_schema(schemas[i]);
free(schemas);
}
} else {
EXPECT_EQ(reader.get_table_schema("test"), nullptr);
}
EXPECT_TRUE(source->failed);
source->fail_at = 0;
auto schema = reader.get_table_schema("test");
ASSERT_NE(schema, nullptr);
EXPECT_EQ(schema->get_table_name(), "test");
}
}
TEST_P(TableReadFailureTest, TimeseriesMetadataPropagatesEveryReadFailure) {
for (bool selected_devices : {false, true}) {
auto invoke = [this, selected_devices](
storage::TsFileReader& reader,
storage::DeviceTimeseriesMetadataMap& result) {
if (!selected_devices)
return reader.get_timeseries_metadata(result);
std::vector<std::shared_ptr<storage::IDeviceID>> devices;
for (int i = 0; i < 5; ++i) {
std::vector<std::string> segments{"test",
"d" + std::to_string(i)};
if (std::get<0>(GetParam()) == 2) segments.push_back("tag");
devices.emplace_back(
new storage::StringArrayDeviceID(segments));
}
return reader.get_timeseries_metadata(devices, result);
};
int first_read = 0;
int last_read = 0;
{
storage::TsFileReader reader;
auto* source = new FailingReadFile(bytes_);
ASSERT_EQ(
reader.open(
std::unique_ptr<storage::RandomAccessReadFile>(source)),
common::E_OK);
first_read = source->reads + 1;
storage::DeviceTimeseriesMetadataMap result;
ASSERT_EQ(invoke(reader, result), common::E_OK);
ASSERT_EQ(result.size(),
static_cast<size_t>(std::get<1>(GetParam())));
last_read = source->reads;
ASSERT_GE(last_read, first_read);
}
for (bool persistent : {false, true}) {
for (bool short_read : {false, true}) {
for (int fail_at = first_read; fail_at <= last_read;
++fail_at) {
SCOPED_TRACE(::testing::Message()
<< selected_devices << ":" << persistent << ":"
<< short_read << ":" << fail_at);
storage::TsFileReader reader;
auto* source = new FailingReadFile(bytes_);
ASSERT_EQ(
reader.open(
std::unique_ptr<storage::RandomAccessReadFile>(
source)),
common::E_OK);
source->fail_at = fail_at;
source->persistent = persistent;
source->short_read = short_read;
storage::DeviceTimeseriesMetadataMap result;
result[nullptr] = {};
EXPECT_EQ(invoke(reader, result), common::E_FILE_READ_ERR);
EXPECT_TRUE(source->failed);
EXPECT_TRUE(result.empty());
source->fail_at = 0;
ASSERT_EQ(invoke(reader, result), common::E_OK);
EXPECT_EQ(result.size(),
static_cast<size_t>(std::get<1>(GetParam())));
}
}
}
}
}
TEST_P(TableReadFailureTest, CMetadataPropagatesEveryReadFailure) {
std::vector<std::string> names;
for (int i = 0; i < 5; ++i) names.push_back("d" + std::to_string(i));
DeviceID devices[5]{};
char* segments[5][3]{};
for (int i = 0; i < 5; ++i) {
segments[i][0] = const_cast<char*>("test");
segments[i][1] = const_cast<char*>(names[i].c_str());
segments[i][2] = const_cast<char*>("tag");
devices[i].segment_count = 1 + std::get<0>(GetParam());
devices[i].segments = segments[i];
}
for (bool selected : {false, true}) {
auto invoke = [&](storage::TsFileReader& reader) {
DeviceTimeseriesMetadataMap result{};
const int ret =
selected ? tsfile_reader_get_timeseries_metadata_for_devices(
&reader, devices, 5, &result)
: tsfile_reader_get_timeseries_metadata_all(&reader,
&result);
if (ret == common::E_OK) {
EXPECT_EQ(result.device_count,
static_cast<uint32_t>(std::get<1>(GetParam())));
EXPECT_NE(result.entries, nullptr);
} else {
EXPECT_EQ(result.device_count, 0u);
EXPECT_EQ(result.entries, nullptr);
}
tsfile_free_device_timeseries_metadata_map(&result);
return ret;
};
int first_read = 0;
int last_read = 0;
{
storage::TsFileReader reader;
auto* source = new FailingReadFile(bytes_);
ASSERT_EQ(
reader.open(
std::unique_ptr<storage::RandomAccessReadFile>(source)),
common::E_OK);
first_read = source->reads + 1;
ASSERT_EQ(invoke(reader), common::E_OK);
last_read = source->reads;
ASSERT_GE(last_read, first_read);
}
for (bool persistent : {false, true}) {
for (int mode = 0; mode < 2; ++mode) {
for (int fail_at = first_read; fail_at <= last_read;
++fail_at) {
SCOPED_TRACE(::testing::Message()
<< selected << ":" << persistent << ":" << mode
<< ":" << fail_at);
storage::TsFileReader reader;
auto* source = new FailingReadFile(bytes_);
ASSERT_EQ(
reader.open(
std::unique_ptr<storage::RandomAccessReadFile>(
source)),
common::E_OK);
source->fail_at = fail_at;
source->persistent = persistent;
source->short_read = mode == 1;
EXPECT_EQ(invoke(reader), common::E_FILE_READ_ERR);
EXPECT_TRUE(source->failed);
source->fail_at = 0;
ASSERT_EQ(invoke(reader), common::E_OK);
}
}
}
}
}
TEST_P(TableReadFailureTest, CMetadataCopyExceptionsClearOutput) {
storage::TsFileReader reader;
ASSERT_EQ(reader.open(std::unique_ptr<storage::RandomAccessReadFile>(
new FailingReadFile(bytes_))),
common::E_OK);
DeviceID* devices = nullptr;
uint32_t count = 0;
ASSERT_EQ(tsfile_reader_get_all_devices(&reader, &devices, &count),
common::E_OK);
ASSERT_GE(count, 2u);
for (bool selected : {false, true}) {
auto invoke = [&](DeviceTimeseriesMetadataMap* result) {
return selected ? tsfile_reader_get_timeseries_metadata_for_devices(
&reader, devices, count, result)
: tsfile_reader_get_timeseries_metadata_all(&reader,
result);
};
for (bool device_copy : {false, true}) {
for (bool oom : {false, true}) {
DeviceTimeseriesMetadataMap result{};
{
// Fail after one entry has been copied to exercise cleanup
// of both completed and partially populated C structures.
const char* point =
device_copy ? (oom ? "device_id_copy_oom"
: "device_id_copy_error")
: (oom ? "timeseries_metadata_copy_oom"
: "timeseries_metadata_copy_error");
InjectionGuard injection(point, 1);
EXPECT_EQ(invoke(&result),
oom ? common::E_OOM : common::E_FILE_READ_ERR);
EXPECT_EQ(result.entries, nullptr);
EXPECT_EQ(result.device_count, 0u);
tsfile_free_device_timeseries_metadata_map(&result);
}
ASSERT_EQ(invoke(&result), common::E_OK);
EXPECT_EQ(result.device_count, count);
tsfile_free_device_timeseries_metadata_map(&result);
}
}
}
tsfile_free_device_id_array(devices, count);
}
TEST_P(TableReadFailureTest, TimeseriesMetadataValidatesInputs) {
storage::TsFileReader reader;
storage::DeviceTimeseriesMetadataMap result;
result[nullptr] = {};
EXPECT_EQ(reader.get_timeseries_metadata(result), common::E_INVALID_ARG);
EXPECT_TRUE(result.empty());
const std::vector<std::shared_ptr<storage::IDeviceID>> no_devices;
result[nullptr] = {};
EXPECT_EQ(reader.get_timeseries_metadata(no_devices, result),
common::E_INVALID_ARG);
EXPECT_TRUE(result.empty());
ASSERT_EQ(reader.open(std::unique_ptr<storage::RandomAccessReadFile>(
new FailingReadFile(bytes_))),
common::E_OK);
EXPECT_EQ(reader.get_timeseries_metadata(no_devices, result), common::E_OK);
EXPECT_TRUE(result.empty());
result[nullptr] = {};
EXPECT_EQ(reader.get_timeseries_metadata({nullptr}, result),
common::E_INVALID_ARG);
EXPECT_TRUE(result.empty());
DeviceTimeseriesMetadataMap c_result{};
EXPECT_EQ(tsfile_reader_get_timeseries_metadata_all(nullptr, &c_result),
common::E_INVALID_ARG);
EXPECT_EQ(c_result.entries, nullptr);
EXPECT_EQ(c_result.device_count, 0u);
char* invalid_segments[] = {nullptr, const_cast<char*>("d0")};
DeviceID invalid_device{};
invalid_device.path = const_cast<char*>("test.d0");
invalid_device.segment_count = 2;
invalid_device.segments = invalid_segments;
EXPECT_EQ(tsfile_reader_get_timeseries_metadata_for_devices(
&reader, &invalid_device, 1, &c_result),
common::E_INVALID_ARG);
invalid_device.segments = nullptr;
EXPECT_EQ(tsfile_reader_get_timeseries_metadata_for_devices(
&reader, &invalid_device, 1, &c_result),
common::E_INVALID_ARG);
}
TEST_P(TableReadFailureTest, ColumnArrayAllocationFailureReturnsOOM) {
for (bool all_tables : {false, true}) {
SCOPED_TRACE(all_tables);
storage::TsFileReader reader;
ASSERT_EQ(reader.open(std::unique_ptr<storage::RandomAccessReadFile>(
new FailingReadFile(bytes_))),
common::E_OK);
InjectionGuard injection("table_schema_column_alloc_fail");
if (all_tables) {
TableSchema* schemas = nullptr;
uint32_t size = 1;
EXPECT_EQ(tsfile_reader_get_all_table_schemas_checked(
&reader, &schemas, &size),
common::E_OOM);
EXPECT_EQ(schemas, nullptr);
EXPECT_EQ(size, 0u);
} else {
TableSchema schema{};
EXPECT_EQ(tsfile_reader_get_table_schema_checked(&reader, "test",
&schema),
common::E_OOM);
EXPECT_EQ(schema.table_name, nullptr);
EXPECT_EQ(schema.column_num, 0);
EXPECT_EQ(schema.column_schemas, nullptr);
free_table_schema(schema);
}
}
}
TEST_P(TableReadFailureTest, SchemaCopyExceptionsClearOutput) {
for (bool oom : {false, true}) {
for (bool all_tables : {false, true}) {
SCOPED_TRACE(all_tables);
storage::TsFileReader reader;
ASSERT_EQ(
reader.open(std::unique_ptr<storage::RandomAccessReadFile>(
new FailingReadFile(bytes_))),
common::E_OK);
InjectionGuard injection(oom ? "table_schema_copy_oom"
: "table_schema_copy_error");
if (all_tables) {
TableSchema* schemas = nullptr;
uint32_t size = 1;
EXPECT_EQ(tsfile_reader_get_all_table_schemas_checked(
&reader, &schemas, &size),
oom ? common::E_OOM : common::E_FILE_READ_ERR);
EXPECT_EQ(schemas, nullptr);
EXPECT_EQ(size, 0u);
} else {
TableSchema schema{};
EXPECT_EQ(tsfile_reader_get_table_schema_checked(
&reader, "test", &schema),
oom ? common::E_OOM : common::E_FILE_READ_ERR);
EXPECT_EQ(schema.table_name, nullptr);
EXPECT_EQ(schema.column_num, 0);
EXPECT_EQ(schema.column_schemas, nullptr);
free_table_schema(schema);
}
}
}
}
TEST_P(TableReadFailureTest, WholeFileMetadataUpdatesCallerOutput) {
for (bool fail : {false, true}) {
SCOPED_TRACE(fail);
FailingReadFile source(bytes_);
source.fail_at = fail ? 1 : 0;
storage::TsFileIOReader io_reader;
ASSERT_EQ(io_reader.init(&source), common::E_OK);
storage::MetadataQuerier querier(&io_reader);
storage::IMetadataQuerier& interface = querier;
storage::TsFileMeta* metadata = nullptr;
if (fail) {
metadata = reinterpret_cast<storage::TsFileMeta*>(&querier);
EXPECT_EQ(interface.get_whole_file_metadata(metadata),
common::E_FILE_READ_ERR);
EXPECT_EQ(metadata, nullptr);
} else {
ASSERT_EQ(interface.get_whole_file_metadata(metadata),
common::E_OK);
ASSERT_NE(metadata, nullptr);
EXPECT_EQ(metadata->table_schemas_.count("test"), 1u);
}
}
}
TEST_P(TableReadFailureTest, NullTagOperatorsAcceptNullValues) {
storage::TsFileReader reader;
ASSERT_EQ(reader.open(std::unique_ptr<storage::RandomAccessReadFile>(
new FailingReadFile(bytes_))),
common::E_OK);
for (TagFilterOp op : {TAG_FILTER_IS_NULL, TAG_FILTER_IS_NOT_NULL}) {
for (bool checked : {false, true}) {
SCOPED_TRACE(::testing::Message() << op << ":" << checked);
TagFilterHandle filter = nullptr;
ERRNO ret = common::E_OK;
if (checked) {
ret = tsfile_tag_filter_create_checked(&reader, "test", "id0",
nullptr, op, &filter);
} else {
filter = tsfile_tag_filter_create(&reader, "test", "id0",
nullptr, op, &ret);
}
ASSERT_EQ(ret, common::E_OK);
ASSERT_NE(filter, nullptr);
storage::ResultSet* result = nullptr;
ret = reader.query("test", {"value"}, 0, INT64_MAX, result,
static_cast<storage::Filter*>(filter));
EXPECT_EQ(ret, common::E_OK);
int rows = 0;
bool next = false;
if (result != nullptr) {
while ((ret = result->next(next)) == common::E_OK && next) {
++rows;
}
reader.destroy_query_data_set(result);
}
tsfile_tag_filter_free(filter);
EXPECT_EQ(ret, common::E_OK);
EXPECT_EQ(rows, op == TAG_FILTER_IS_NULL
? 0
: std::get<1>(GetParam()) * 30);
}
}
for (TagFilterOp op : {TAG_FILTER_EQ, TAG_FILTER_NEQ, TAG_FILTER_LT,
TAG_FILTER_LTEQ, TAG_FILTER_GT, TAG_FILTER_GTEQ,
TAG_FILTER_REGEXP, TAG_FILTER_NOT_REGEXP}) {
TagFilterHandle filter = &reader;
EXPECT_EQ(tsfile_tag_filter_create_checked(&reader, "test", "id0",
nullptr, op, &filter),
common::E_INVALID_ARG);
EXPECT_EQ(filter, nullptr);
}
}
TEST_P(TableReadFailureTest, LegacyTagFactoriesReturnNullOnReadFailure) {
using Factory = TagFilterHandle (*)(TsFileReader, const char*, const char*,
const char*);
const Factory factories[] = {tsfile_tag_filter_eq, tsfile_tag_filter_neq,
tsfile_tag_filter_lt, tsfile_tag_filter_lteq,
tsfile_tag_filter_gt, tsfile_tag_filter_gteq};
for (auto factory : factories) {
storage::TsFileReader reader;
auto* source = new FailingReadFile(bytes_);
ASSERT_EQ(
reader.open(std::unique_ptr<storage::RandomAccessReadFile>(source)),
common::E_OK);
source->fail_at = source->reads + 1;
source->persistent = true;
TagFilterHandle filter = factory(&reader, "test", "id0", "d0");
EXPECT_EQ(filter, nullptr);
tsfile_tag_filter_free(filter);
EXPECT_TRUE(source->failed);
source->fail_at = 0;
filter = factory(&reader, "test", "id0", "d0");
ASSERT_NE(filter, nullptr);
tsfile_tag_filter_free(filter);
}
}
TEST_P(TableReadFailureTest, CheckedTagFactoriesPreserveMissingTableError) {
storage::TsFileReader reader;
ASSERT_EQ(reader.open(std::unique_ptr<storage::RandomAccessReadFile>(
new FailingReadFile(bytes_))),
common::E_OK);
TagFilterHandle filter = &reader;
EXPECT_EQ(tsfile_tag_filter_create_checked(&reader, "missing", "id0", "d0",
TAG_FILTER_EQ, &filter),
common::E_TABLE_NOT_EXIST);
ASSERT_EQ(filter, nullptr);
filter = &reader;
EXPECT_EQ(tsfile_tag_filter_between_checked(&reader, "missing", "id0", "d0",
"d1", false, &filter),
common::E_TABLE_NOT_EXIST);
ASSERT_EQ(filter, nullptr);
ERRNO code = common::E_OK;
EXPECT_EQ(tsfile_tag_filter_create(&reader, "missing", "id0", "d0",
TAG_FILTER_EQ, &code),
nullptr);
EXPECT_EQ(code, common::E_INVALID_ARG);
code = common::E_OK;
EXPECT_EQ(tsfile_tag_filter_between(&reader, "missing", "id0", "d0", "d1",
false, &code),
nullptr);
EXPECT_EQ(code, common::E_INVALID_ARG);
}
INSTANTIATE_TEST_SUITE_P(LeafAndInternalDeviceIndexes, TableReadFailureTest,
::testing::Combine(::testing::Values(1, 2),
::testing::Values(2, 5)));
TEST(TableMetadataReadFailureTest, ResizedBufferIsReleasedOnReadFailure) {
// A metadata tail larger than the initial 1 KiB buffer forces another
// read before deserialization. Inject the failure into that second read.
std::vector<char> bytes(2048, 0);
const uint32_t metadata_size = 1536;
for (int i = 0; i < 4; ++i) {
bytes[bytes.size() - 10 + i] =
static_cast<char>(metadata_size >> (8 * (3 - i)));
}
for (int failure = 0; failure < 3; ++failure) {
SCOPED_TRACE(failure);
const int64_t allocated_before =
common::ModStat::get_instance().get_stat(common::MOD_TSFILE_READER);
{
FailingReadFile source(bytes);
source.fail_at = 2;
source.short_read = failure == 1;
storage::TsFileIOReader reader;
ASSERT_EQ(reader.init(&source), common::E_OK);
storage::TsFileMeta* metadata = nullptr;
if (failure == 2) {
common::TEST_fail_next_mem_realloc();
EXPECT_EQ(reader.get_tsfile_meta(metadata), common::E_OOM);
} else {
EXPECT_EQ(reader.get_tsfile_meta(metadata),
common::E_FILE_READ_ERR);
}
EXPECT_EQ(source.reads, failure == 2 ? 1 : 2);
}
EXPECT_EQ(
common::ModStat::get_instance().get_stat(common::MOD_TSFILE_READER),
allocated_before);
}
}
class FailingBlockReader : public storage::TsBlockReader {
public:
int has_next(bool& next) override {
next = true;
return common::E_OK;
}
int next(common::TsBlock*& block) override {
++reads;
block = nullptr;
return common::E_FILE_READ_ERR;
}
void close() override {}
int reads = 0;
};
TEST(TableResultReadFailureTest, FailureWhileFetchingBlockIsTerminal) {
storage::libtsfile_init();
for (int mode : {storage::RETURN_ROW, storage::RETURN_BATCH}) {
auto* source = new FailingBlockReader();
storage::TableResultSet result(
std::unique_ptr<storage::TsBlockReader>(source), {"value"},
{common::INT64}, mode);
bool next = true;
common::TsBlock* block = nullptr;
if (mode == storage::RETURN_ROW) {
EXPECT_EQ(result.next(next), common::E_FILE_READ_ERR);
EXPECT_FALSE(next);
} else {
EXPECT_EQ(result.get_next_tsblock(block), common::E_FILE_READ_ERR);
EXPECT_EQ(block, nullptr);
}
EXPECT_EQ(result.next(next), common::E_FILE_READ_ERR);
EXPECT_FALSE(next);
EXPECT_EQ(result.get_next_tsblock(block), common::E_FILE_READ_ERR);
EXPECT_EQ(block, nullptr);
EXPECT_EQ(source->reads, 1);
}
storage::libtsfile_destroy();
}
} // namespace