|
33 | 33 | #include "iceberg/avro/avro_register.h" |
34 | 34 | #include "iceberg/constants.h" |
35 | 35 | #include "iceberg/expression/expressions.h" |
| 36 | +#include "iceberg/file_io.h" |
36 | 37 | #include "iceberg/partition_spec.h" |
37 | 38 | #include "iceberg/schema.h" |
38 | 39 | #include "iceberg/table_metadata.h" |
|
50 | 51 |
|
51 | 52 | namespace iceberg { |
52 | 53 |
|
| 54 | +namespace { |
| 55 | + |
| 56 | +struct ManifestWriteFailureState { |
| 57 | + int manifest_output_count = 0; |
| 58 | + int fail_on_manifest_output = 0; |
| 59 | + int manifest_close_count = 0; |
| 60 | + int fail_on_manifest_close = 0; |
| 61 | + std::vector<std::string> manifest_outputs; |
| 62 | + std::vector<std::string> manifest_close_attempts; |
| 63 | + std::vector<std::string> deleted_files; |
| 64 | +}; |
| 65 | + |
| 66 | +class ManifestWriteFailureOutputStream : public PositionOutputStream { |
| 67 | + public: |
| 68 | + ManifestWriteFailureOutputStream(std::unique_ptr<PositionOutputStream> delegate, |
| 69 | + std::string location, |
| 70 | + std::shared_ptr<ManifestWriteFailureState> state) |
| 71 | + : delegate_(std::move(delegate)), |
| 72 | + location_(std::move(location)), |
| 73 | + state_(std::move(state)) {} |
| 74 | + |
| 75 | + Result<int64_t> Position() const override { return delegate_->Position(); } |
| 76 | + |
| 77 | + Result<int64_t> StoredLength() const override { return delegate_->StoredLength(); } |
| 78 | + |
| 79 | + Status Write(std::span<const std::byte> data) override { |
| 80 | + return delegate_->Write(data); |
| 81 | + } |
| 82 | + |
| 83 | + Status Flush() override { return delegate_->Flush(); } |
| 84 | + |
| 85 | + Status Close() override { |
| 86 | + if (closed_) { |
| 87 | + return {}; |
| 88 | + } |
| 89 | + closed_ = true; |
| 90 | + state_->manifest_close_attempts.push_back(location_); |
| 91 | + ++state_->manifest_close_count; |
| 92 | + ICEBERG_RETURN_UNEXPECTED(delegate_->Close()); |
| 93 | + if (state_->manifest_close_count == state_->fail_on_manifest_close) { |
| 94 | + return IOError("Injected rewrite manifest close failure"); |
| 95 | + } |
| 96 | + return {}; |
| 97 | + } |
| 98 | + |
| 99 | + private: |
| 100 | + std::unique_ptr<PositionOutputStream> delegate_; |
| 101 | + std::string location_; |
| 102 | + std::shared_ptr<ManifestWriteFailureState> state_; |
| 103 | + bool closed_{false}; |
| 104 | +}; |
| 105 | + |
| 106 | +class ManifestWriteFailureOutputFile : public OutputFile { |
| 107 | + public: |
| 108 | + ManifestWriteFailureOutputFile(std::unique_ptr<OutputFile> delegate, |
| 109 | + std::string location, |
| 110 | + std::shared_ptr<ManifestWriteFailureState> state) |
| 111 | + : delegate_(std::move(delegate)), |
| 112 | + location_(std::move(location)), |
| 113 | + state_(std::move(state)) {} |
| 114 | + |
| 115 | + std::string_view location() const override { return location_; } |
| 116 | + |
| 117 | + Result<std::unique_ptr<PositionOutputStream>> Create() override { |
| 118 | + ICEBERG_ASSIGN_OR_RAISE(auto stream, delegate_->Create()); |
| 119 | + return std::make_unique<ManifestWriteFailureOutputStream>(std::move(stream), |
| 120 | + location_, state_); |
| 121 | + } |
| 122 | + |
| 123 | + Result<std::unique_ptr<PositionOutputStream>> CreateOrOverwrite() override { |
| 124 | + ICEBERG_ASSIGN_OR_RAISE(auto stream, delegate_->CreateOrOverwrite()); |
| 125 | + return std::make_unique<ManifestWriteFailureOutputStream>(std::move(stream), |
| 126 | + location_, state_); |
| 127 | + } |
| 128 | + |
| 129 | + private: |
| 130 | + std::unique_ptr<OutputFile> delegate_; |
| 131 | + std::string location_; |
| 132 | + std::shared_ptr<ManifestWriteFailureState> state_; |
| 133 | +}; |
| 134 | + |
| 135 | +class ManifestWriteFailureFileIO : public FileIO { |
| 136 | + public: |
| 137 | + ManifestWriteFailureFileIO(std::shared_ptr<FileIO> delegate, |
| 138 | + std::string metadata_location, |
| 139 | + std::shared_ptr<ManifestWriteFailureState> state) |
| 140 | + : delegate_(std::move(delegate)), |
| 141 | + metadata_location_(std::move(metadata_location)), |
| 142 | + state_(std::move(state)) {} |
| 143 | + |
| 144 | + Result<std::unique_ptr<InputFile>> NewInputFile(std::string file_location) override { |
| 145 | + return delegate_->NewInputFile(std::move(file_location)); |
| 146 | + } |
| 147 | + |
| 148 | + Result<std::unique_ptr<InputFile>> NewInputFile(std::string file_location, |
| 149 | + size_t length) override { |
| 150 | + return delegate_->NewInputFile(std::move(file_location), length); |
| 151 | + } |
| 152 | + |
| 153 | + Result<std::unique_ptr<OutputFile>> NewOutputFile(std::string file_location) override { |
| 154 | + const bool is_manifest_output = IsManifestOutput(file_location); |
| 155 | + if (is_manifest_output) { |
| 156 | + state_->manifest_outputs.push_back(file_location); |
| 157 | + ++state_->manifest_output_count; |
| 158 | + if (state_->manifest_output_count == state_->fail_on_manifest_output) { |
| 159 | + return IOError("Injected rewrite manifest writer failure"); |
| 160 | + } |
| 161 | + } |
| 162 | + ICEBERG_ASSIGN_OR_RAISE(auto output_file, delegate_->NewOutputFile(file_location)); |
| 163 | + if (is_manifest_output) { |
| 164 | + return std::make_unique<ManifestWriteFailureOutputFile>( |
| 165 | + std::move(output_file), std::move(file_location), state_); |
| 166 | + } |
| 167 | + return output_file; |
| 168 | + } |
| 169 | + |
| 170 | + Status DeleteFile(const std::string& file_location) override { |
| 171 | + state_->deleted_files.push_back(file_location); |
| 172 | + return delegate_->DeleteFile(file_location); |
| 173 | + } |
| 174 | + |
| 175 | + Status DeleteFiles(const std::vector<std::string>& file_locations) override { |
| 176 | + state_->deleted_files.insert(state_->deleted_files.end(), file_locations.begin(), |
| 177 | + file_locations.end()); |
| 178 | + return delegate_->DeleteFiles(file_locations); |
| 179 | + } |
| 180 | + |
| 181 | + private: |
| 182 | + bool IsManifestOutput(const std::string& file_location) const { |
| 183 | + return file_location.starts_with(metadata_location_) && |
| 184 | + file_location.ends_with(".avro"); |
| 185 | + } |
| 186 | + |
| 187 | + std::shared_ptr<FileIO> delegate_; |
| 188 | + std::string metadata_location_; |
| 189 | + std::shared_ptr<ManifestWriteFailureState> state_; |
| 190 | +}; |
| 191 | + |
| 192 | +} // namespace |
| 193 | + |
53 | 194 | class RewriteManifestsTest : public MinimalUpdateTestBase, |
54 | 195 | public ::testing::WithParamInterface<int8_t> { |
55 | 196 | protected: |
@@ -208,6 +349,14 @@ class RewriteManifestsTest : public MinimalUpdateTestBase, |
208 | 349 | mock_catalog_ = std::move(mock_catalog); |
209 | 350 | } |
210 | 351 |
|
| 352 | + void BindTableWithFileIO(std::shared_ptr<FileIO> file_io) { |
| 353 | + ICEBERG_UNWRAP_OR_FAIL(auto bound_table, |
| 354 | + Table::Make(table_->name(), table_->metadata(), |
| 355 | + std::string(table_->metadata_file_location()), |
| 356 | + std::move(file_io), catalog_)); |
| 357 | + table_ = std::move(bound_table); |
| 358 | + } |
| 359 | + |
211 | 360 | void ValidateSummary(const std::shared_ptr<Snapshot>& snapshot, int replaced, int kept, |
212 | 361 | int created, int entry_count) { |
213 | 362 | ASSERT_NE(snapshot, nullptr); |
@@ -720,6 +869,115 @@ TEST_P(RewriteManifestsTest, ReplaceManifestsMaxSize) { |
720 | 869 | {ManifestStatus::kExisting}, {append_snapshot_id}); |
721 | 870 | } |
722 | 871 |
|
| 872 | +TEST_P(RewriteManifestsTest, CleansRolledRewriteManifestsAfterApplyFailure) { |
| 873 | + ASSERT_THAT(AppendFiles({file_a_, file_b_, file_c_}), IsOk()); |
| 874 | + |
| 875 | + ICEBERG_UNWRAP_OR_FAIL(auto before, CurrentManifests()); |
| 876 | + ASSERT_EQ(before.size(), 1U); |
| 877 | + |
| 878 | + ICEBERG_UNWRAP_OR_FAIL(auto props, table_->NewUpdateProperties()); |
| 879 | + props->Set(std::string(TableProperties::kManifestTargetSizeBytes.key()), "1"); |
| 880 | + EXPECT_THAT(props->Commit(), IsOk()); |
| 881 | + EXPECT_THAT(table_->Refresh(), IsOk()); |
| 882 | + |
| 883 | + auto failure_state = std::make_shared<ManifestWriteFailureState>(); |
| 884 | + failure_state->fail_on_manifest_output = 2; |
| 885 | + BindTableWithFileIO(std::make_shared<ManifestWriteFailureFileIO>( |
| 886 | + file_io_, table_location_ + "/metadata/", failure_state)); |
| 887 | + |
| 888 | + ICEBERG_UNWRAP_OR_FAIL(auto rewrite, table_->NewRewriteManifests()); |
| 889 | + rewrite->ClusterBy([](const DataFile&) { return "file"; }); |
| 890 | + |
| 891 | + auto result = rewrite->Commit(); |
| 892 | + EXPECT_THAT(result, IsError(ErrorKind::kIOError)); |
| 893 | + EXPECT_THAT(result, HasErrorMessage("Injected rewrite manifest writer failure")); |
| 894 | + |
| 895 | + ASSERT_GE(failure_state->manifest_outputs.size(), 2U); |
| 896 | + const auto& closed_manifest_path = failure_state->manifest_outputs[0]; |
| 897 | + EXPECT_THAT(failure_state->deleted_files, ::testing::Contains(closed_manifest_path)); |
| 898 | + EXPECT_FALSE(FileExists(closed_manifest_path)); |
| 899 | +} |
| 900 | + |
| 901 | +TEST_P(RewriteManifestsTest, ClosesOpenRewriteWritersAfterApplyFailure) { |
| 902 | + ASSERT_THAT(AppendFiles({file_a_, file_b_, file_c_}), IsOk()); |
| 903 | + |
| 904 | + auto failure_state = std::make_shared<ManifestWriteFailureState>(); |
| 905 | + failure_state->fail_on_manifest_output = 3; |
| 906 | + BindTableWithFileIO(std::make_shared<ManifestWriteFailureFileIO>( |
| 907 | + file_io_, table_location_ + "/metadata/", failure_state)); |
| 908 | + |
| 909 | + ICEBERG_UNWRAP_OR_FAIL(auto rewrite, table_->NewRewriteManifests()); |
| 910 | + rewrite->ClusterBy([](const DataFile& file) { return file.file_path; }); |
| 911 | + |
| 912 | + auto result = rewrite->Commit(); |
| 913 | + EXPECT_THAT(result, IsError(ErrorKind::kIOError)); |
| 914 | + EXPECT_THAT(result, HasErrorMessage("Injected rewrite manifest writer failure")); |
| 915 | + |
| 916 | + ASSERT_EQ(failure_state->manifest_outputs.size(), 3U); |
| 917 | + EXPECT_THAT(failure_state->manifest_close_attempts, |
| 918 | + ::testing::UnorderedElementsAre(failure_state->manifest_outputs[0], |
| 919 | + failure_state->manifest_outputs[1])); |
| 920 | + for (size_t i = 0; i < 2; ++i) { |
| 921 | + EXPECT_THAT(failure_state->deleted_files, |
| 922 | + ::testing::Contains(failure_state->manifest_outputs[i])); |
| 923 | + EXPECT_FALSE(FileExists(failure_state->manifest_outputs[i])); |
| 924 | + } |
| 925 | +} |
| 926 | + |
| 927 | +TEST_P(RewriteManifestsTest, ClosesAllRewriteWritersAfterCloseFailure) { |
| 928 | + ASSERT_THAT(AppendFiles({file_a_, file_b_, file_c_}), IsOk()); |
| 929 | + |
| 930 | + auto failure_state = std::make_shared<ManifestWriteFailureState>(); |
| 931 | + failure_state->fail_on_manifest_close = 1; |
| 932 | + BindTableWithFileIO(std::make_shared<ManifestWriteFailureFileIO>( |
| 933 | + file_io_, table_location_ + "/metadata/", failure_state)); |
| 934 | + |
| 935 | + ICEBERG_UNWRAP_OR_FAIL(auto rewrite, table_->NewRewriteManifests()); |
| 936 | + rewrite->ClusterBy([](const DataFile& file) { return file.file_path; }); |
| 937 | + |
| 938 | + auto result = rewrite->Commit(); |
| 939 | + EXPECT_THAT(result, IsError(ErrorKind::kIOError)); |
| 940 | + EXPECT_THAT(result, HasErrorMessage("Injected rewrite manifest close failure")); |
| 941 | + |
| 942 | + ASSERT_EQ(failure_state->manifest_outputs.size(), 3U); |
| 943 | + ASSERT_EQ(failure_state->manifest_close_attempts.size(), 3U); |
| 944 | + EXPECT_THAT(failure_state->manifest_close_attempts, |
| 945 | + ::testing::UnorderedElementsAre(failure_state->manifest_outputs[0], |
| 946 | + failure_state->manifest_outputs[1], |
| 947 | + failure_state->manifest_outputs[2])); |
| 948 | + for (size_t i = 0; i < failure_state->manifest_close_attempts.size(); ++i) { |
| 949 | + const auto& closed_manifest_path = failure_state->manifest_close_attempts[i]; |
| 950 | + EXPECT_THAT(failure_state->deleted_files, ::testing::Contains(closed_manifest_path)); |
| 951 | + EXPECT_FALSE(FileExists(closed_manifest_path)); |
| 952 | + } |
| 953 | +} |
| 954 | + |
| 955 | +TEST_P(RewriteManifestsTest, PreservesRewriteFailureWhenClosingWritersFails) { |
| 956 | + ASSERT_THAT(AppendFiles({file_a_, file_b_, file_c_}), IsOk()); |
| 957 | + |
| 958 | + auto failure_state = std::make_shared<ManifestWriteFailureState>(); |
| 959 | + failure_state->fail_on_manifest_output = 3; |
| 960 | + failure_state->fail_on_manifest_close = 1; |
| 961 | + BindTableWithFileIO(std::make_shared<ManifestWriteFailureFileIO>( |
| 962 | + file_io_, table_location_ + "/metadata/", failure_state)); |
| 963 | + |
| 964 | + ICEBERG_UNWRAP_OR_FAIL(auto rewrite, table_->NewRewriteManifests()); |
| 965 | + rewrite->ClusterBy([](const DataFile& file) { return file.file_path; }); |
| 966 | + |
| 967 | + auto result = rewrite->Commit(); |
| 968 | + EXPECT_THAT(result, IsError(ErrorKind::kIOError)); |
| 969 | + EXPECT_THAT(result, HasErrorMessage("Injected rewrite manifest writer failure")); |
| 970 | + EXPECT_THAT(result, HasErrorMessage("additionally failed to close manifest writer")); |
| 971 | + EXPECT_THAT(result, HasErrorMessage("Injected rewrite manifest close failure")); |
| 972 | + |
| 973 | + ASSERT_EQ(failure_state->manifest_outputs.size(), 3U); |
| 974 | + ASSERT_EQ(failure_state->manifest_close_attempts.size(), 2U); |
| 975 | + for (const auto& manifest_path : failure_state->manifest_outputs) { |
| 976 | + EXPECT_THAT(failure_state->deleted_files, ::testing::Contains(manifest_path)); |
| 977 | + EXPECT_FALSE(FileExists(manifest_path)); |
| 978 | + } |
| 979 | +} |
| 980 | + |
723 | 981 | TEST_P(RewriteManifestsTest, ConcurrentRewriteManifest) { |
724 | 982 | ASSERT_THAT(AppendFiles({file_a_}), IsOk()); |
725 | 983 | ICEBERG_UNWRAP_OR_FAIL(auto append_snapshot_a, table_->current_snapshot()); |
|
0 commit comments