Skip to content

Commit 92feae1

Browse files
authored
fix: fast append should not contain deleted file during retry process (#706)
1 parent 8bbda0c commit 92feae1

6 files changed

Lines changed: 139 additions & 27 deletions

File tree

src/iceberg/test/fast_append_test.cc

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,20 @@
2020
#include "iceberg/update/fast_append.h"
2121

2222
#include <format>
23+
#include <optional>
24+
#include <string>
25+
#include <vector>
2326

2427
#include <gmock/gmock.h>
2528
#include <gtest/gtest.h>
2629

2730
#include "iceberg/avro/avro_register.h"
31+
#include "iceberg/constants.h"
32+
#include "iceberg/manifest/manifest_entry.h"
33+
#include "iceberg/manifest/manifest_writer.h"
2834
#include "iceberg/partition_spec.h"
2935
#include "iceberg/schema.h"
36+
#include "iceberg/snapshot.h"
3037
#include "iceberg/table_metadata.h"
3138
#include "iceberg/test/matchers.h"
3239
#include "iceberg/test/test_resource.h"
@@ -72,6 +79,23 @@ class FastAppendTest : public UpdateTestBase {
7279
return data_file;
7380
}
7481

82+
Result<ManifestFile> WriteManifest(
83+
const std::string& path, const std::vector<std::shared_ptr<DataFile>>& files) {
84+
ICEBERG_ASSIGN_OR_RAISE(
85+
auto writer, ManifestWriter::MakeWriter(table_->metadata()->format_version,
86+
kInvalidSnapshotId, path, file_io_, spec_,
87+
schema_, ManifestContent::kData));
88+
for (const auto& file : files) {
89+
ManifestEntry entry;
90+
entry.status = ManifestStatus::kAdded;
91+
entry.snapshot_id = std::nullopt;
92+
entry.data_file = file;
93+
ICEBERG_RETURN_UNEXPECTED(writer->WriteAddedEntry(entry));
94+
}
95+
ICEBERG_RETURN_UNEXPECTED(writer->Close());
96+
return writer->ToManifestFile();
97+
}
98+
7599
std::shared_ptr<PartitionSpec> spec_;
76100
std::shared_ptr<Schema> schema_;
77101
std::shared_ptr<DataFile> file_a_;
@@ -90,6 +114,9 @@ TEST_F(FastAppendTest, AppendDataFile) {
90114
EXPECT_EQ(snapshot->summary.at("added-data-files"), "1");
91115
EXPECT_EQ(snapshot->summary.at("added-records"), "100");
92116
EXPECT_EQ(snapshot->summary.at("added-files-size"), "1024");
117+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kManifestsCreated), "1");
118+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kManifestsKept), "0");
119+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kManifestsReplaced), "0");
93120
}
94121

95122
TEST_F(FastAppendTest, AppendMultipleDataFiles) {
@@ -172,6 +199,40 @@ TEST_F(FastAppendTest, FinalizeIgnoresCleanupDeleteFailure) {
172199
IsOk());
173200
}
174201

202+
TEST_F(FastAppendTest, RetryCopiesAppendManifestAgain) {
203+
table_->metadata()->format_version = 1;
204+
const auto path = table_location_ + "/metadata/input.avro";
205+
ICEBERG_UNWRAP_OR_FAIL(auto manifest, WriteManifest(path, {file_a_}));
206+
207+
std::shared_ptr<FastAppend> fast_append;
208+
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
209+
std::vector<std::string> deleted_paths;
210+
fast_append->DeleteWith([&](const std::string& deleted_path) {
211+
deleted_paths.push_back(deleted_path);
212+
return file_io_->DeleteFile(deleted_path);
213+
});
214+
fast_append->AppendManifest(manifest);
215+
216+
auto& update = static_cast<SnapshotUpdate&>(*fast_append);
217+
// First Apply() copies the input manifest because v1 cannot inherit snapshot IDs.
218+
ICEBERG_UNWRAP_OR_FAIL(auto first_apply, update.Apply());
219+
SnapshotCache first_cache(first_apply.snapshot.get());
220+
ICEBERG_UNWRAP_OR_FAIL(auto first_manifests, first_cache.Manifests(file_io_));
221+
ASSERT_EQ(first_manifests.size(), 1U);
222+
const auto first_rewritten_path = first_manifests[0].manifest_path;
223+
EXPECT_NE(first_rewritten_path, path);
224+
225+
// Second Apply() simulates retry cleanup, then copies the original manifest again.
226+
ICEBERG_UNWRAP_OR_FAIL(auto second_apply, update.Apply());
227+
EXPECT_THAT(deleted_paths, testing::Contains(first_rewritten_path));
228+
229+
SnapshotCache second_cache(second_apply.snapshot.get());
230+
ICEBERG_UNWRAP_OR_FAIL(auto second_manifests, second_cache.Manifests(file_io_));
231+
ASSERT_EQ(second_manifests.size(), 1U);
232+
EXPECT_NE(second_manifests[0].manifest_path, path);
233+
EXPECT_NE(second_manifests[0].manifest_path, first_rewritten_path);
234+
}
235+
175236
TEST_F(FastAppendTest, AppendDuplicateFile) {
176237
std::shared_ptr<FastAppend> fast_append;
177238
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());

src/iceberg/update/fast_append.cc

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
#include "iceberg/manifest/manifest_entry.h"
2727
#include "iceberg/manifest/manifest_util_internal.h"
2828
#include "iceberg/snapshot.h"
29-
#include "iceberg/table.h"
29+
#include "iceberg/table.h" // IWYU pragma: keep
3030
#include "iceberg/table_metadata.h"
3131
#include "iceberg/table_properties.h"
3232
#include "iceberg/transaction.h"
@@ -58,7 +58,7 @@ FastAppend& FastAppend::AppendFile(const std::shared_ptr<DataFile>& file) {
5858
auto [iter, inserted] = data_files.insert(file);
5959
if (inserted) {
6060
has_new_files_ = true;
61-
ICEBERG_BUILDER_RETURN_IF_ERROR(summary_.AddedFile(*spec, *file));
61+
ICEBERG_BUILDER_RETURN_IF_ERROR(added_data_files_summary_.AddedFile(*spec, *file));
6262
}
6363

6464
return *this;
@@ -75,11 +75,13 @@ FastAppend& FastAppend::AppendManifest(const ManifestFile& manifest) {
7575
"Sequence number must be assigned during commit");
7676

7777
if (can_inherit_snapshot_id() && manifest.added_snapshot_id == kInvalidSnapshotId) {
78-
summary_.AddedManifest(manifest);
78+
appended_manifests_summary_.AddedManifest(manifest);
7979
append_manifests_.push_back(manifest);
8080
} else {
8181
// The manifest must be rewritten with this update's snapshot ID
82-
ICEBERG_BUILDER_ASSIGN_OR_RETURN(auto copied_manifest, CopyManifest(manifest));
82+
ICEBERG_BUILDER_ASSIGN_OR_RETURN(auto copied_manifest,
83+
CopyManifest(manifest, /*update_summary=*/true));
84+
append_manifests_to_copy_.push_back(manifest);
8385
rewritten_append_manifests_.push_back(std::move(copied_manifest));
8486
}
8587

@@ -93,6 +95,16 @@ Result<std::vector<ManifestFile>> FastAppend::Apply(
9395
std::vector<ManifestFile> manifests;
9496

9597
ICEBERG_ASSIGN_OR_RAISE(auto new_written_manifests, WriteNewManifests());
98+
// A retry cleanup deletes copied append manifests and clears the rewritten
99+
// list; rebuild them from the original appended manifests before re-applying.
100+
if (rewritten_append_manifests_.empty() && !append_manifests_to_copy_.empty()) {
101+
for (const auto& manifest : append_manifests_to_copy_) {
102+
ICEBERG_ASSIGN_OR_RAISE(auto copied_manifest,
103+
CopyManifest(manifest, /*update_summary=*/false));
104+
rewritten_append_manifests_.push_back(std::move(copied_manifest));
105+
}
106+
}
107+
96108
manifests.reserve(new_written_manifests.size() + append_manifests_.size() +
97109
rewritten_append_manifests_.size());
98110
if (!new_written_manifests.empty()) {
@@ -122,15 +134,31 @@ Result<std::vector<ManifestFile>> FastAppend::Apply(
122134
snapshot_manifests.end());
123135
}
124136

137+
manifest_count_summary_ =
138+
BuildManifestCountSummary(manifests, /*replaced_manifests_count=*/0);
139+
125140
return manifests;
126141
}
127142

128143
std::unordered_map<std::string, std::string> FastAppend::Summary() {
144+
summary_.Clear();
129145
summary_.SetPartitionSummaryLimit(
130146
base().properties.Get(TableProperties::kWritePartitionSummaryLimit));
147+
summary_.Merge(added_data_files_summary_);
148+
summary_.Merge(appended_manifests_summary_);
149+
for (const auto& [property, value] : custom_summary_properties_) {
150+
summary_.Set(property, value);
151+
}
152+
summary_.Merge(manifest_count_summary_);
131153
return summary_.Build();
132154
}
133155

156+
void FastAppend::SetSummaryProperty(const std::string& property,
157+
const std::string& value) {
158+
custom_summary_properties_[property] = value;
159+
SnapshotUpdate::SetSummaryProperty(property, value);
160+
}
161+
134162
Status FastAppend::CleanUncommitted(const std::unordered_set<std::string>& committed) {
135163
// Clean up new manifests that were written but not committed
136164
if (!new_manifests_.empty()) {
@@ -151,24 +179,26 @@ Status FastAppend::CleanUncommitted(const std::unordered_set<std::string>& commi
151179
std::ignore = DeleteFile(manifest.manifest_path);
152180
}
153181
}
182+
rewritten_append_manifests_.clear();
154183
}
155184
return {};
156185
}
157186

158187
bool FastAppend::CleanupAfterCommit() const {
159-
// Cleanup after committing is disabled for FastAppend unless there are
160-
// rewritten_append_manifests_ because:
161-
// 1.) Appended manifests are never rewritten
188+
// Cleanup after committing is disabled for FastAppend unless append manifests
189+
// were copied or need to be copied on retry because:
190+
// 1.) Directly appended manifests are never rewritten
162191
// 2.) Manifests which are written out as part of AppendFile are already cleaned
163192
// up between commit attempts in WriteNewManifests
164-
return !rewritten_append_manifests_.empty();
193+
return !rewritten_append_manifests_.empty() || !append_manifests_to_copy_.empty();
165194
}
166195

167196
Result<std::shared_ptr<PartitionSpec>> FastAppend::Spec(int32_t spec_id) {
168197
return base().PartitionSpecById(spec_id);
169198
}
170199

171-
Result<ManifestFile> FastAppend::CopyManifest(const ManifestFile& manifest) {
200+
Result<ManifestFile> FastAppend::CopyManifest(const ManifestFile& manifest,
201+
bool update_summary) {
172202
const TableMetadata& current = base();
173203
ICEBERG_ASSIGN_OR_RAISE(auto schema, current.Schema());
174204
ICEBERG_ASSIGN_OR_RAISE(auto spec,
@@ -180,7 +210,8 @@ Result<ManifestFile> FastAppend::CopyManifest(const ManifestFile& manifest) {
180210

181211
// Copy the manifest with the new snapshot ID.
182212
return CopyAppendManifest(manifest, ctx_->table->io(), schema, spec, snapshot_id,
183-
new_manifest_path, current.format_version, &summary_);
213+
new_manifest_path, current.format_version,
214+
update_summary ? &appended_manifests_summary_ : nullptr);
184215
}
185216

186217
Result<std::vector<ManifestFile>> FastAppend::WriteNewManifests() {

src/iceberg/update/fast_append.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class ICEBERG_EXPORT FastAppend : public SnapshotUpdate {
7272
const TableMetadata& metadata_to_update,
7373
const std::shared_ptr<Snapshot>& snapshot) override;
7474
std::unordered_map<std::string, std::string> Summary() override;
75+
void SetSummaryProperty(const std::string& property, const std::string& value) override;
7576
Status CleanUncommitted(const std::unordered_set<std::string>& committed) override;
7677
bool CleanupAfterCommit() const override;
7778

@@ -84,8 +85,9 @@ class ICEBERG_EXPORT FastAppend : public SnapshotUpdate {
8485
/// \brief Copy a manifest file with a new snapshot ID.
8586
///
8687
/// \param manifest The manifest to copy
88+
/// \param update_summary Whether to add copied entries to the append summary
8789
/// \return The copied manifest file
88-
Result<ManifestFile> CopyManifest(const ManifestFile& manifest);
90+
Result<ManifestFile> CopyManifest(const ManifestFile& manifest, bool update_summary);
8991

9092
/// \brief Write new manifests for the accumulated data files.
9193
///
@@ -95,9 +97,18 @@ class ICEBERG_EXPORT FastAppend : public SnapshotUpdate {
9597
private:
9698
std::string table_name_;
9799
std::unordered_map<int32_t, DataFileSet> new_data_files_by_spec_;
100+
// Stable input summaries for retry-safe summary_ rebuilds.
101+
SnapshotSummaryBuilder added_data_files_summary_;
102+
SnapshotSummaryBuilder appended_manifests_summary_;
103+
// User-provided summary properties restored after summary_ rebuilds.
104+
std::unordered_map<std::string, std::string> custom_summary_properties_;
98105
std::vector<ManifestFile> append_manifests_;
106+
// Original manifests kept to recreate copied manifests after retry cleanup.
107+
std::vector<ManifestFile> append_manifests_to_copy_;
99108
std::vector<ManifestFile> rewritten_append_manifests_;
100109
std::vector<ManifestFile> new_manifests_;
110+
// Manifest count summary from the latest Apply() result.
111+
SnapshotSummaryBuilder manifest_count_summary_;
101112
bool has_new_files_{false};
102113
};
103114

src/iceberg/update/merging_snapshot_update.cc

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -944,26 +944,11 @@ Result<std::vector<ManifestFile>> MergingSnapshotUpdate::Apply(
944944
result.insert(result.end(), std::make_move_iterator(merged_deletes.begin()),
945945
std::make_move_iterator(merged_deletes.end()));
946946

947-
// Manifest count summary: unassigned manifests count as neither created nor kept.
948-
int32_t manifests_created = 0;
949-
int32_t manifests_kept = 0;
950-
for (const auto& m : result) {
951-
if (m.added_snapshot_id == snapshot_id) {
952-
++manifests_created;
953-
} else if (m.added_snapshot_id != kInvalidSnapshotId) {
954-
++manifests_kept;
955-
}
956-
}
957947
int32_t replaced_manifests_count = data_filter_manager_->ReplacedManifestsCount() +
958948
delete_filter_manager_->ReplacedManifestsCount() +
959949
data_merge_manager_->ReplacedManifestsCount() +
960950
delete_merge_manager_->ReplacedManifestsCount();
961-
summary_builder().Set(SnapshotSummaryFields::kManifestsCreated,
962-
std::to_string(manifests_created));
963-
summary_builder().Set(SnapshotSummaryFields::kManifestsKept,
964-
std::to_string(manifests_kept));
965-
summary_builder().Set(SnapshotSummaryFields::kManifestsReplaced,
966-
std::to_string(replaced_manifests_count));
951+
summary_builder().Merge(BuildManifestCountSummary(result, replaced_manifests_count));
967952

968953
return result;
969954
}

src/iceberg/update/snapshot_update.cc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,28 @@ std::string SnapshotUpdate::ManifestListPath() {
424424
return ctx_->MetadataFileLocation(filename);
425425
}
426426

427+
SnapshotSummaryBuilder SnapshotUpdate::BuildManifestCountSummary(
428+
std::span<const ManifestFile> manifests, int32_t replaced_manifests_count) {
429+
SnapshotSummaryBuilder summary;
430+
int32_t manifests_created = 0;
431+
int32_t manifests_kept = 0;
432+
int64_t snapshot_id = SnapshotId();
433+
for (const auto& manifest : manifests) {
434+
if (manifest.added_snapshot_id == snapshot_id) {
435+
++manifests_created;
436+
} else if (manifest.added_snapshot_id != kInvalidSnapshotId) {
437+
++manifests_kept;
438+
}
439+
}
440+
441+
summary.Set(SnapshotSummaryFields::kManifestsCreated,
442+
std::to_string(manifests_created));
443+
summary.Set(SnapshotSummaryFields::kManifestsKept, std::to_string(manifests_kept));
444+
summary.Set(SnapshotSummaryFields::kManifestsReplaced,
445+
std::to_string(replaced_manifests_count));
446+
return summary;
447+
}
448+
427449
std::string SnapshotUpdate::ManifestPath() {
428450
// Generate manifest path
429451
// Format: {metadata_location}/{uuid}-m{manifest_count}.avro

src/iceberg/update/snapshot_update.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ class ICEBERG_EXPORT SnapshotUpdate : public PendingUpdate {
216216
std::string ManifestPath();
217217
std::string ManifestListPath();
218218
SnapshotSummaryBuilder& summary_builder() { return summary_; }
219+
SnapshotSummaryBuilder BuildManifestCountSummary(
220+
std::span<const ManifestFile> manifests, int32_t replaced_manifests_count);
219221

220222
private:
221223
/// \brief Returns the snapshot summary from the implementation and updates totals.

0 commit comments

Comments
 (0)