Skip to content

Commit ecc0058

Browse files
committed
fix: clean up failed rewrite manifest outputs
1 parent d4ba26b commit ecc0058

3 files changed

Lines changed: 344 additions & 39 deletions

File tree

src/iceberg/test/rewrite_manifests_test.cc

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "iceberg/avro/avro_register.h"
3434
#include "iceberg/constants.h"
3535
#include "iceberg/expression/expressions.h"
36+
#include "iceberg/file_io.h"
3637
#include "iceberg/partition_spec.h"
3738
#include "iceberg/schema.h"
3839
#include "iceberg/table_metadata.h"
@@ -50,6 +51,146 @@
5051

5152
namespace iceberg {
5253

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+
53194
class RewriteManifestsTest : public MinimalUpdateTestBase,
54195
public ::testing::WithParamInterface<int8_t> {
55196
protected:
@@ -208,6 +349,14 @@ class RewriteManifestsTest : public MinimalUpdateTestBase,
208349
mock_catalog_ = std::move(mock_catalog);
209350
}
210351

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+
211360
void ValidateSummary(const std::shared_ptr<Snapshot>& snapshot, int replaced, int kept,
212361
int created, int entry_count) {
213362
ASSERT_NE(snapshot, nullptr);
@@ -720,6 +869,115 @@ TEST_P(RewriteManifestsTest, ReplaceManifestsMaxSize) {
720869
{ManifestStatus::kExisting}, {append_snapshot_id});
721870
}
722871

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+
723981
TEST_P(RewriteManifestsTest, ConcurrentRewriteManifest) {
724982
ASSERT_THAT(AppendFiles({file_a_}), IsOk());
725983
ICEBERG_UNWRAP_OR_FAIL(auto append_snapshot_a, table_->current_snapshot());

0 commit comments

Comments
 (0)