Skip to content

Commit 93577b3

Browse files
authored
feat: parallelize writing manifests (#778)
1 parent 881b3b5 commit 93577b3

8 files changed

Lines changed: 304 additions & 66 deletions

File tree

src/iceberg/avro/avro_writer.cc

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#include <memory>
2323

24+
#include <arrow/array.h>
2425
#include <arrow/array/builder_base.h>
2526
#include <arrow/c/bridge.h>
2627
#include <arrow/record_batch.h>
@@ -178,12 +179,6 @@ class GenericDatumBackend : public AvroWriteBackend {
178179

179180
class AvroWriter::Impl {
180181
public:
181-
~Impl() {
182-
if (arrow_schema_.release != nullptr) {
183-
ArrowSchemaRelease(&arrow_schema_);
184-
}
185-
}
186-
187182
Status Open(const WriterOptions& options) {
188183
write_schema_ = options.schema;
189184

@@ -227,19 +222,22 @@ class AvroWriter::Impl {
227222
options.properties.Get(WriterProperties::kAvroSyncInterval), codec,
228223
compression_level, metadata));
229224

230-
ICEBERG_RETURN_UNEXPECTED(ToArrowSchema(*write_schema_, &arrow_schema_));
225+
ArrowSchema c_schema;
226+
ICEBERG_RETURN_UNEXPECTED(ToArrowSchema(*write_schema_, &c_schema));
227+
ICEBERG_ARROW_ASSIGN_OR_RETURN(arrow_schema_, ::arrow::ImportSchema(&c_schema));
231228
return {};
232229
}
233230

234231
Status Write(ArrowArray* data) {
235-
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto result,
236-
::arrow::ImportArray(data, &arrow_schema_));
232+
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto batch,
233+
::arrow::ImportRecordBatch(data, arrow_schema_));
237234

238-
for (int64_t i = 0; i < result->length(); i++) {
239-
ICEBERG_RETURN_UNEXPECTED(backend_->WriteRow(*write_schema_, *result, i));
235+
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto struct_array, batch->ToStructArray());
236+
for (int64_t i = 0; i < struct_array->length(); i++) {
237+
ICEBERG_RETURN_UNEXPECTED(backend_->WriteRow(*write_schema_, *struct_array, i));
240238
}
241239

242-
num_records_ += result->length();
240+
num_records_ += struct_array->length();
243241
return {};
244242
}
245243

@@ -278,8 +276,8 @@ class AvroWriter::Impl {
278276
std::shared_ptr<::avro::ValidSchema> avro_schema_;
279277
// Arrow output stream of the Avro file to write
280278
std::shared_ptr<::arrow::io::OutputStream> arrow_output_stream_;
281-
// Arrow schema to write data.
282-
ArrowSchema arrow_schema_;
279+
// Arrow schema to import C data batches.
280+
std::shared_ptr<::arrow::Schema> arrow_schema_;
283281
// Total length of the written Avro file.
284282
int64_t total_bytes_ = 0;
285283
// Number of records written.

src/iceberg/manifest/manifest_adapter.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ Status ManifestAdapter::StartAppending() {
126126
return InvalidArgument("Adapter buffer not empty, cannot start appending.");
127127
}
128128
array_ = {};
129-
size_ = 0;
130129
ArrowError error;
131130
ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
132131
ArrowArrayInitFromSchema(&array_, &schema_, &error), error);
@@ -138,6 +137,7 @@ Result<ArrowArray*> ManifestAdapter::FinishAppending() {
138137
ArrowError error;
139138
ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
140139
ArrowArrayFinishBuildingDefault(&array_, &error), error);
140+
size_ = 0;
141141
return &array_;
142142
}
143143

src/iceberg/test/executor_util_test.cc

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
#include <atomic>
2121
#include <concepts>
2222
#include <functional>
23+
#include <memory>
2324
#include <optional>
2425
#include <ranges>
26+
#include <span>
2527
#include <string>
2628
#include <tuple>
2729
#include <unordered_map>
@@ -51,19 +53,15 @@ struct IntTask {
5153
}
5254
};
5355

54-
struct BoolTask {
55-
Result<std::vector<bool>> operator()(bool) {
56-
return Result<std::vector<bool>>{std::vector<bool>{}};
57-
}
58-
};
59-
6056
static_assert(internal::ParallelCollectible<std::vector<int>&, IntTask>);
6157
static_assert(
6258
std::same_as<decltype(ParallelCollect(std::nullopt, std::declval<std::vector<int>&>(),
6359
IntTask{})),
6460
Result<std::vector<int>>>);
65-
static_assert(!internal::ParallelCollectible<decltype(std::views::iota(0, 3)), IntTask>);
66-
static_assert(!internal::ParallelCollectible<std::vector<bool>&, BoolTask>);
61+
static_assert(internal::ParallelCollectible<decltype(std::views::iota(0, 3)), IntTask>);
62+
static_assert(std::same_as<decltype(ParallelCollect(std::nullopt, std::views::iota(0, 3),
63+
IntTask{})),
64+
Result<std::vector<int>>>);
6765

6866
} // namespace
6967

@@ -139,6 +137,45 @@ TEST(ParallelCollectTest, CollectsSingleRange) {
139137
EXPECT_THAT(*result, UnorderedElementsAre(2, 4, 6));
140138
}
141139

140+
TEST(ParallelCollectTest, CollectsIotaView) {
141+
auto input = std::views::iota(1, 4);
142+
143+
auto result = ParallelCollect(std::nullopt, input, [](int value) {
144+
return Result<std::unordered_set<int>>{{value * 2}};
145+
});
146+
147+
EXPECT_THAT(result, IsOk());
148+
EXPECT_THAT(*result, UnorderedElementsAre(2, 4, 6));
149+
}
150+
151+
TEST(ParallelCollectTest, CollectsTransformView) {
152+
std::vector<int> values = {1, 2, 3, 4};
153+
std::span<const int> files(values);
154+
auto input = std::views::iota(0, 2) | std::views::transform([files](int index) {
155+
return files.subspan(index * 2, 2);
156+
});
157+
158+
auto result = ParallelCollect(std::nullopt, input, [](std::span<const int> group) {
159+
return Result<std::vector<int>>{{group.front(), group.back()}};
160+
});
161+
162+
EXPECT_THAT(result, IsOk());
163+
EXPECT_THAT(*result, ElementsAre(1, 2, 3, 4));
164+
}
165+
166+
TEST(ParallelCollectTest, CollectsMoveOnlyPrvalues) {
167+
auto input = std::views::iota(1, 4) | std::views::transform([](int value) {
168+
return std::make_unique<int>(value);
169+
});
170+
171+
auto result = ParallelCollect(std::nullopt, input, [](std::unique_ptr<int> value) {
172+
return Result<std::vector<int>>{{*value}};
173+
});
174+
175+
EXPECT_THAT(result, IsOk());
176+
EXPECT_THAT(*result, ElementsAre(1, 2, 3));
177+
}
178+
142179
TEST(ParallelCollectTest, KeepsTupleResultFromSingleRange) {
143180
std::vector<int> input = {1, 2};
144181

src/iceberg/test/fast_append_test.cc

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

2222
#include <format>
23+
#include <limits>
2324
#include <optional>
2425
#include <string>
2526
#include <thread>
@@ -32,14 +33,18 @@
3233
#include "iceberg/avro/avro_register.h"
3334
#include "iceberg/constants.h"
3435
#include "iceberg/manifest/manifest_entry.h"
36+
#include "iceberg/manifest/manifest_reader.h"
3537
#include "iceberg/manifest/manifest_writer.h"
3638
#include "iceberg/partition_spec.h"
3739
#include "iceberg/schema.h"
3840
#include "iceberg/snapshot.h"
3941
#include "iceberg/table_metadata.h"
42+
#include "iceberg/table_properties.h"
43+
#include "iceberg/test/executor.h"
4044
#include "iceberg/test/matchers.h"
4145
#include "iceberg/test/update_test_base.h"
4246
#include "iceberg/transaction.h"
47+
#include "iceberg/update/update_properties.h" // IWYU pragma: keep
4348

4449
namespace iceberg {
4550

@@ -117,6 +122,29 @@ class FastAppendTest : public UpdateTestBase {
117122
return writer->ToManifestFile();
118123
}
119124

125+
void SetManifestTargetSizeBytes(int64_t size_bytes) {
126+
ICEBERG_UNWRAP_OR_FAIL(auto props, table_->NewUpdateProperties());
127+
props->Set(std::string(TableProperties::kManifestTargetSizeBytes.key()),
128+
std::to_string(size_bytes));
129+
EXPECT_THAT(props->Commit(), IsOk());
130+
EXPECT_THAT(table_->Refresh(), IsOk());
131+
}
132+
133+
Result<std::vector<ManifestFile>> CurrentDataManifests() {
134+
ICEBERG_ASSIGN_OR_RAISE(auto snapshot, table_->current_snapshot());
135+
SnapshotCache snapshot_cache(snapshot.get());
136+
ICEBERG_ASSIGN_OR_RAISE(auto manifests, snapshot_cache.DataManifests(file_io_));
137+
return std::vector<ManifestFile>(manifests.begin(), manifests.end());
138+
}
139+
140+
Result<std::vector<ManifestEntry>> ReadEntries(const ManifestFile& manifest) {
141+
ICEBERG_ASSIGN_OR_RAISE(
142+
auto spec, table_->metadata()->PartitionSpecById(manifest.partition_spec_id));
143+
ICEBERG_ASSIGN_OR_RAISE(auto reader,
144+
ManifestReader::Make(manifest, file_io_, schema_, spec));
145+
return reader->Entries();
146+
}
147+
120148
std::shared_ptr<PartitionSpec> spec_;
121149
std::shared_ptr<Schema> schema_;
122150
std::shared_ptr<DataFile> file_a_;
@@ -183,6 +211,63 @@ TEST_F(FastAppendTest, AppendManyFiles) {
183211
EXPECT_EQ(snapshot->summary.at("added-files-size"), std::to_string(total_size));
184212
}
185213

214+
TEST_F(FastAppendTest, WriteManifestGroups) {
215+
SetManifestTargetSizeBytes(std::numeric_limits<int64_t>::max());
216+
217+
test::ThreadExecutor executor;
218+
std::shared_ptr<FastAppend> fast_append;
219+
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
220+
fast_append->WriteManifestsWith(executor, 3);
221+
222+
constexpr size_t kFileCount = 15'000;
223+
constexpr size_t kGroupSize = 7'500;
224+
std::vector<std::shared_ptr<DataFile>> files;
225+
files.reserve(kFileCount);
226+
for (size_t index = 0; index < kFileCount; ++index) {
227+
auto data_file =
228+
CreateDataFile(std::format("/data/group_{}.parquet", index),
229+
/*record_count=*/1, /*size=*/1, static_cast<int64_t>(index % 2));
230+
fast_append->AppendFile(data_file);
231+
files.push_back(std::move(data_file));
232+
}
233+
234+
EXPECT_THAT(fast_append->Commit(), IsOk());
235+
236+
EXPECT_THAT(table_->Refresh(), IsOk());
237+
EXPECT_EQ(executor.submit_count(), 2);
238+
ICEBERG_UNWRAP_OR_FAIL(auto manifests, CurrentDataManifests());
239+
ASSERT_EQ(manifests.size(), 2U);
240+
241+
for (size_t group_index = 0; group_index < manifests.size(); ++group_index) {
242+
ASSERT_TRUE(manifests[group_index].added_files_count.has_value());
243+
EXPECT_EQ(manifests[group_index].added_files_count.value(), kGroupSize);
244+
245+
ICEBERG_UNWRAP_OR_FAIL(auto entries, ReadEntries(manifests[group_index]));
246+
ASSERT_EQ(entries.size(), kGroupSize);
247+
const size_t offset = group_index * kGroupSize;
248+
for (size_t entry_index = 0; entry_index < entries.size(); ++entry_index) {
249+
ASSERT_NE(entries[entry_index].data_file, nullptr);
250+
EXPECT_EQ(entries[entry_index].data_file->file_path,
251+
files[offset + entry_index]->file_path);
252+
}
253+
}
254+
}
255+
256+
TEST_F(FastAppendTest, InvalidManifestParallelism) {
257+
test::ThreadExecutor executor;
258+
std::shared_ptr<FastAppend> fast_append;
259+
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
260+
fast_append->WriteManifestsWith(executor, 0);
261+
fast_append->AppendFile(file_a_);
262+
263+
auto result = fast_append->Commit();
264+
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
265+
EXPECT_THAT(
266+
result,
267+
HasErrorMessage("Manifest write parallelism must be greater than 0, but was: 0"));
268+
EXPECT_EQ(executor.submit_count(), 0);
269+
}
270+
186271
TEST_F(FastAppendTest, EmptyTableAppendUpdatesSequenceNumbers) {
187272
EXPECT_THAT(table_->current_snapshot(), HasErrorMessage("No current snapshot"));
188273
const int64_t base_sequence_number = table_->metadata()->last_sequence_number;

src/iceberg/test/merging_snapshot_update_test.cc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919

2020
#include "iceberg/update/merging_snapshot_update.h"
2121

22+
#include <limits>
2223
#include <memory>
24+
#include <optional>
25+
#include <ranges>
2326
#include <string>
2427
#include <unordered_set>
2528
#include <vector>
@@ -40,6 +43,7 @@
4043
#include "iceberg/table.h"
4144
#include "iceberg/table_metadata.h"
4245
#include "iceberg/table_properties.h"
46+
#include "iceberg/test/executor.h"
4347
#include "iceberg/test/matchers.h"
4448
#include "iceberg/test/update_test_base.h"
4549
#include "iceberg/transaction.h"
@@ -92,6 +96,16 @@ class TestMergeAppend : public MergingSnapshotUpdate {
9296
Result<std::shared_ptr<PartitionSpec>> DataSpec() const {
9397
return MergingSnapshotUpdate::DataSpec();
9498
}
99+
Result<std::vector<ManifestFile>> WriteDeletesForTest(
100+
std::span<const std::shared_ptr<DataFile>> files,
101+
const std::shared_ptr<PartitionSpec>& spec) {
102+
auto entries = files | std::views::transform([](const auto& file) {
103+
return ContentFileWithSequenceNumber{
104+
.file = file, .data_sequence_number = std::nullopt};
105+
}) |
106+
std::ranges::to<std::vector>();
107+
return WriteDeleteManifests(entries, spec);
108+
}
95109
int64_t GeneratedSnapshotId() { return SnapshotId(); }
96110
void SetDataSeqNumber(int64_t seq) { SetNewDataFilesDataSequenceNumber(seq); }
97111
void SetCaseSensitive(bool case_sensitive) { CaseSensitive(case_sensitive); }
@@ -266,6 +280,14 @@ class MergingSnapshotUpdateTest : public MinimalUpdateTestBase {
266280
table_->metadata()->format_version = format_version;
267281
}
268282

283+
void SetManifestTargetSizeBytes(int64_t size_bytes) {
284+
ICEBERG_UNWRAP_OR_FAIL(auto props, table_->NewUpdateProperties());
285+
props->Set(std::string(TableProperties::kManifestTargetSizeBytes.key()),
286+
std::to_string(size_bytes));
287+
EXPECT_THAT(props->Commit(), IsOk());
288+
EXPECT_THAT(table_->Refresh(), IsOk());
289+
}
290+
269291
// Commit file_a_ with FastAppend and refresh the table.
270292
void CommitFileA() {
271293
ICEBERG_UNWRAP_OR_FAIL(auto fa, table_->NewFastAppend());
@@ -639,6 +661,29 @@ TEST_F(MergingSnapshotUpdateTest, AddDeleteFileWithExplicitSequenceWritesSequenc
639661
EXPECT_EQ(entries[0].sequence_number.value(), 17);
640662
}
641663

664+
TEST_F(MergingSnapshotUpdateTest, WriteDeleteGroups) {
665+
SetManifestTargetSizeBytes(std::numeric_limits<int64_t>::max());
666+
667+
test::ThreadExecutor executor;
668+
ICEBERG_UNWRAP_OR_FAIL(auto op, NewMergeAppend());
669+
op->WriteManifestsWith(executor, 3);
670+
671+
constexpr size_t kFileCount = 15'000;
672+
auto files = std::views::iota(0UZ, kFileCount) |
673+
std::views::transform([this](size_t index) {
674+
return MakeDeleteFile(std::format("/delete/group_{}.parquet", index),
675+
static_cast<int64_t>(index % 2));
676+
}) |
677+
std::ranges::to<std::vector>();
678+
ICEBERG_UNWRAP_OR_FAIL(auto manifests, op->WriteDeletesForTest(files, spec_));
679+
680+
EXPECT_EQ(executor.submit_count(), 2);
681+
ASSERT_EQ(manifests.size(), 2U);
682+
for (const auto& manifest : manifests) {
683+
EXPECT_EQ(manifest.content, ManifestContent::kDeletes);
684+
}
685+
}
686+
642687
TEST_F(MergingSnapshotUpdateTest, ApplyRebuildsDeleteSummaryAfterPreparingDeletes) {
643688
auto del_file = MakeDeleteFile("/delete/del_a.parquet", 1L);
644689

0 commit comments

Comments
 (0)