wangyong9999 commented on code in PR #227:
URL: https://github.com/apache/paimon-cpp/pull/227#discussion_r3859526697
##########
src/paimon/core/operation/abstract_file_store_write.cpp:
##########
@@ -188,6 +190,22 @@ Result<std::vector<std::shared_ptr<CommitMessage>>>
AbstractFileStoreWrite::Prep
}
std::vector<std::shared_ptr<CommitMessage>> result;
+ // A writer hands its files over the moment it drains an increment, so
once a message exists
+ // nothing the writer does removes its data files, sidecars or managed
blob packs. Failing
+ // out of this loop never hands `result` to the caller, so the messages
built so far would
+ // be stranded with no one left to clean them up.
+ ScopeGuard prepare_guard([this, &result]() {
+ if (result.empty()) {
+ return;
+ }
+ Status status = UncommittedFileCleaner::Delete(
Review Comment:
`result` may contain a metadata-only upgrade, where `CompactBefore` and
`CompactAfter` reuse the same `file_name`. If a later bucket fails during
prepare, this guard invokes the cleaner, which treats that file as an
uncommitted output and deletes it even though the previous snapshot still
references it, including its `.blobref`. Before deleting `CompactAfter`,
exclude files also present in `CompactBefore`, as `MergeTreeWriter::DoClose`
already does.
##########
src/paimon/core/operation/expire_snapshots.cpp:
##########
@@ -277,14 +280,31 @@ Status ExpireSnapshots::CleanUnusedDataFiles(const
std::string& manifest_list_na
}
}
+ // One data file path factory per (partition, bucket): creating one
re-derives the
+ // partition path and external path provider, too heavy to repeat for
every file.
+ std::unordered_map<BinaryRow, std::map<int32_t,
std::shared_ptr<DataFilePathFactory>>>
+ data_file_path_factories;
std::vector<std::future<void>> futures;
ScopeGuard guard([&futures]() { Wait(futures); });
for (const auto& [data_file_to_delete, entry] : data_files_to_delete) {
- auto delete_file_path = data_file_to_delete;
- futures.push_back(Via(executor_.get(), [this, delete_file_path]() {
- auto status = fs_->Delete(delete_file_path);
- // delete quietly will ignore any status error
- (void)status;
+ // An expired data file takes its companion files with it (e.g.
the managed blob
+ // reference sidecar). CollectFiles resolves their paths, honoring
external data
+ // paths.
+ std::shared_ptr<DataFilePathFactory>& data_file_path_factory =
+ data_file_path_factories[entry.Partition()][entry.Bucket()];
+ if (data_file_path_factory == nullptr) {
+ PAIMON_ASSIGN_OR_RAISE(
+ data_file_path_factory,
+
path_factory_->CreateDataFilePathFactory(entry.Partition(), entry.Bucket()));
+ }
+ std::vector<std::string> delete_file_paths =
Review Comment:
Snapshot expiration deletes only the data file and its `.blobref` here,
while the orphan cleaner always skips `.managed.blob` and does not support
primary-key tables. After an update, first-row merge, or compaction drops the
last descriptor reference, the pack remains forever, so managed BLOB storage
grows without bound under normal workloads. Build a live set from sidecars
reachable from retained snapshots, tags, and branches, then reclaim
unreferenced packs.
##########
src/paimon/core/io/primary_key_blob_externalizer.cpp:
##########
@@ -0,0 +1,353 @@
+/*
+ * 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 "paimon/core/io/primary_key_blob_externalizer.h"
+
+#include <map>
+#include <optional>
+#include <set>
+#include <string>
+#include <utility>
+
+#include "arrow/api.h"
+#include "arrow/c/bridge.h"
+#include "fmt/format.h"
+#include "paimon/common/data/blob_defs.h"
+#include "paimon/common/data/blob_descriptor.h"
+#include "paimon/common/data/blob_utils.h"
+#include "paimon/common/utils/arrow/mem_utils.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/checked_cast.h"
+#include "paimon/core/io/data_file_path_factory.h"
+#include "paimon/defs.h"
+#include "paimon/format/file_format.h"
+#include "paimon/format/file_format_factory.h"
+#include "paimon/format/format_writer.h"
+#include "paimon/format/writer_builder.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/memory/bytes.h"
+
+namespace paimon {
+
+namespace {
+
+/// The format managed blob packs are written in. Named once: the writer is
built through the
+/// format factory, and the same name is what an error has to report.
+constexpr const char kPackFormat[] = "blob";
+
+} // namespace
+
+/// Rolls `.managed.blob` packs for one managed blob field. Each written value
appends one blob
+/// format record to the current pack; the pack is sealed once it reaches the
blob target file
+/// size. Descriptors point straight at the payload bytes inside the pack, so
reading one back
+/// is a single ranged read that needs no pack footer.
+class PrimaryKeyBlobExternalizer::ManagedBlobPackWriter {
+ public:
+ ManagedBlobPackWriter(const CoreOptions& options, const
std::shared_ptr<arrow::Field>& field,
+ const std::shared_ptr<DataFilePathFactory>&
path_factory,
+ std::vector<std::string>* uncommitted_packs,
+ const std::shared_ptr<MemoryPool>& pool)
+ : options_(options),
+ field_(field),
+ path_factory_(path_factory),
+ uncommitted_packs_(uncommitted_packs),
+ target_file_size_(options.GetBlobTargetFileSize()),
+ pool_(pool) {}
+
+ /// Copies row `row` of `column` into the current pack and returns the
serialized
+ /// descriptor of the copied payload. The value may itself be a serialized
descriptor; the
+ /// blob format writer then streams the referenced bytes in,
re-materializing them.
+ Result<PAIMON_UNIQUE_PTR<Bytes>> Write(const
std::shared_ptr<arrow::Array>& column,
+ int64_t row) {
+ if (writer_ == nullptr) {
+ PAIMON_RETURN_NOT_OK(OpenCurrent());
+ }
+
+ std::shared_ptr<arrow::Array> element = column->Slice(row, 1);
+ std::shared_ptr<arrow::Array> pack_row;
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(pack_row,
+
arrow::StructArray::Make({std::move(element)}, {field_}));
+ ArrowArray c_array;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*pack_row,
&c_array));
+ PAIMON_RETURN_NOT_OK(writer_->AddBatch(&c_array));
+
+ // The format writer reports the payload bytes of the record it just
stored, so the
+ // record layout stays inside the format. Asked through the base
interface rather than
+ // by downcasting to the blob writer: the blob format lives in a
plugin library, so a
+ // checked cast would need its type info here, and an unchecked one
would be undefined
+ // behaviour the moment the factory returned anything else.
+ std::optional<std::pair<int64_t, int64_t>> payload_range =
writer_->LastPayloadRange();
+ if (!payload_range) {
+ return Status::Invalid(
+ fmt::format("Managed blob pack {} did not produce a payload
record. The '{}' "
+ "format must store one addressable payload per
record.",
+ current_path_, kPackFormat));
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ std::unique_ptr<BlobDescriptor> descriptor,
+ BlobDescriptor::Create(current_path_, payload_range->first,
payload_range->second));
+ PAIMON_UNIQUE_PTR<Bytes> serialized = descriptor->Serialize(pool_);
+
+ PAIMON_ASSIGN_OR_RAISE(
+ bool reach_target_size,
+ writer_->ReachTargetSize(/*suggested_check=*/true,
target_file_size_));
+ if (reach_target_size) {
+ PAIMON_RETURN_NOT_OK(CloseCurrent());
+ }
+ return serialized;
+ }
+
+ /// Seals the current pack: writes the blob format footer and closes the
stream. The
+ /// underlying stream is closed even when writing the footer fails, so a
failed seal never
+ /// leaks the stream.
+ Status CloseCurrent() {
+ if (writer_ == nullptr) {
+ return Status::OK();
+ }
+ Status status = writer_->Finish();
+ Status close_status = out_->Close();
+ if (status.ok()) {
+ status = close_status;
+ }
+ writer_.reset();
+ out_.reset();
+ current_path_.clear();
+ return status;
+ }
+
+ /// Quietly drops the current pack writer; the file itself is removed
through the
+ /// uncommitted pack list.
+ void AbortCurrent() {
+ if (out_) {
+ [[maybe_unused]] Status status = out_->Close();
+ }
+ writer_.reset();
+ out_.reset();
+ current_path_.clear();
+ }
+
+ private:
+ Status OpenCurrent() {
+ std::string path = path_factory_->NewManagedBlobPath();
+ uncommitted_packs_->push_back(path);
+ // The blob format lives in its own plugin library, so the writer is
created through
+ // the format factory like every other core write path; core must not
reference the
+ // plugin's out-of-line symbols directly.
+ std::map<std::string, std::string> format_options = options_.ToMap();
+ // Managed pack writes never convert fetch failures to NULL payloads
and never
+ // interpret placeholder sentinels: strip the user-facing toggles so
the format
+ // defaults (false) apply.
+ format_options.erase(Options::BLOB_WRITE_NULL_ON_MISSING_FILE);
+ format_options.erase(Options::BLOB_WRITE_NULL_ON_FETCH_FAILURE);
+ BlobDefs::EraseInternalPlaceholderOptions(&format_options);
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileFormat> format,
+ FileFormatFactory::Get(kPackFormat,
format_options));
+ ::ArrowSchema c_schema;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(
+ arrow::ExportSchema(*arrow::schema(arrow::FieldVector{field_}),
&c_schema));
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriterBuilder> writer_builder,
+ format->CreateWriterBuilder(&c_schema,
/*batch_size=*/1));
+ writer_builder->WithMemoryPool(pool_);
+ if (auto* specific_fs_builder =
+ dynamic_cast<SpecificFSWriterBuilder*>(writer_builder.get())) {
+ specific_fs_builder->WithFileSystem(options_.GetFileSystem());
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<OutputStream> out,
+ options_.GetFileSystem()->Create(path,
/*overwrite=*/false));
+ out_ = std::move(out);
+ PAIMON_ASSIGN_OR_RAISE(writer_, writer_builder->Build(out_,
/*compression=*/"none"));
+ current_path_ = path;
+ return Status::OK();
+ }
+
+ CoreOptions options_;
+ std::shared_ptr<arrow::Field> field_;
+ std::shared_ptr<DataFilePathFactory> path_factory_;
+ std::vector<std::string>* uncommitted_packs_;
+ int64_t target_file_size_;
+ std::shared_ptr<MemoryPool> pool_;
+
+ std::string current_path_;
+ std::shared_ptr<OutputStream> out_;
+ std::unique_ptr<FormatWriter> writer_;
+};
+
+Result<std::unique_ptr<PrimaryKeyBlobExternalizer>>
PrimaryKeyBlobExternalizer::Create(
+ const CoreOptions& options, const std::shared_ptr<arrow::Schema>&
value_schema,
+ const std::shared_ptr<DataFilePathFactory>& path_factory,
+ const std::shared_ptr<MemoryPool>& pool) {
+ std::vector<std::string> inline_field_names =
options.GetBlobInlineFields();
+ std::set<std::string> inline_fields(inline_field_names.begin(),
inline_field_names.end());
+ std::vector<std::string> managed_field_names =
+ BlobUtils::ManagedBlobFieldNames(value_schema, inline_fields);
+ if (managed_field_names.empty()) {
+ return std::unique_ptr<PrimaryKeyBlobExternalizer>();
+ }
+ // Guarded by SchemaValidation for tables created through the catalog;
checked again here
+ // because pack rolling cannot work with a non-positive target size.
+ if (options.GetBlobTargetFileSize() <= 0) {
+ return Status::Invalid(
+ fmt::format("Managed blob target file size must be positive, "
+ "but got {}.",
+ options.GetBlobTargetFileSize()));
+ }
+ std::vector<int32_t> managed_field_indices;
+ managed_field_indices.reserve(managed_field_names.size());
+ for (const auto& field_name : managed_field_names) {
+
managed_field_indices.push_back(value_schema->GetFieldIndex(field_name));
+ }
+ auto value_type = arrow::struct_(value_schema->fields());
+ return std::unique_ptr<PrimaryKeyBlobExternalizer>(new
PrimaryKeyBlobExternalizer(
+ options, value_type, std::move(managed_field_indices), path_factory,
pool));
+}
+
+PrimaryKeyBlobExternalizer::PrimaryKeyBlobExternalizer(
+ const CoreOptions& options, const std::shared_ptr<arrow::DataType>&
value_type,
+ std::vector<int32_t> managed_field_indices,
+ const std::shared_ptr<DataFilePathFactory>& path_factory,
+ const std::shared_ptr<MemoryPool>& pool)
+ : options_(options),
+ value_type_(value_type),
+ managed_field_indices_(std::move(managed_field_indices)),
+ path_factory_(path_factory),
+ pool_(pool),
+ arrow_pool_(GetArrowPool(pool)),
+ logger_(Logger::GetLogger("PrimaryKeyBlobExternalizer")) {
+ auto struct_type = checked_pointer_cast<arrow::StructType>(value_type_);
+ pack_writers_.reserve(managed_field_indices_.size());
+ for (int32_t field_index : managed_field_indices_) {
+ pack_writers_.push_back(std::make_unique<ManagedBlobPackWriter>(
+ options_, struct_type->field(field_index), path_factory_,
&uncommitted_packs_, pool_));
+ }
+}
+
+PrimaryKeyBlobExternalizer::~PrimaryKeyBlobExternalizer() {
+ Abort();
+}
+
+Result<std::unique_ptr<RecordBatch>> PrimaryKeyBlobExternalizer::Externalize(
+ std::unique_ptr<RecordBatch>&& moved_batch) {
+ std::unique_ptr<RecordBatch> batch = std::move(moved_batch);
+ Result<std::unique_ptr<RecordBatch>> result = [&]() ->
Result<std::unique_ptr<RecordBatch>> {
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array>
arrow_array,
+ arrow::ImportArray(batch->GetData(),
value_type_));
+ auto struct_array =
std::dynamic_pointer_cast<arrow::StructArray>(arrow_array);
+ if (struct_array == nullptr) {
+ return Status::Invalid(
+ "PrimaryKeyBlobExternalizer expects a StructArray record
batch.");
+ }
+ const std::vector<RecordBatch::RowKind>& row_kinds =
batch->GetRowKind();
+ if (!row_kinds.empty() &&
+ static_cast<int64_t>(row_kinds.size()) != struct_array->length()) {
+ return Status::Invalid(
+ "PrimaryKeyBlobExternalizer batch row kinds do not match the
row count.");
+ }
+ arrow::ArrayVector new_children = struct_array->fields();
+ for (size_t writer_index = 0; writer_index <
managed_field_indices_.size();
+ writer_index++) {
+ int32_t field_index = managed_field_indices_[writer_index];
+ const auto& column = struct_array->field(field_index);
+ auto blob_column =
std::dynamic_pointer_cast<arrow::LargeBinaryArray>(column);
+ if (blob_column == nullptr) {
+ return Status::Invalid(
+ fmt::format("PrimaryKeyBlobExternalizer expects managed
blob column {} to be a "
+ "LargeBinaryArray.",
+
struct_array->struct_type()->field(field_index)->name()));
+ }
+ // The member pool, not a local one: these buffers enter the write
buffer and
+ // must stay allocatable-from until the batch is flushed and
released.
+ arrow::LargeBinaryBuilder builder(arrow_pool_.get());
+
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Reserve(blob_column->length()));
+ for (int64_t row = 0; row < blob_column->length(); row++) {
+ bool retract =
+ !row_kinds.empty() && (row_kinds[row] ==
RecordBatch::RowKind::UPDATE_BEFORE ||
+ row_kinds[row] ==
RecordBatch::RowKind::DELETE);
+ // A retract row never keeps a payload: the managed value is
dropped without
+ // writing anything to a pack.
+ if (retract || blob_column->IsNull(row)) {
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.AppendNull());
+ continue;
+ }
+ PAIMON_ASSIGN_OR_RAISE(PAIMON_UNIQUE_PTR<Bytes> descriptor,
+
pack_writers_[writer_index]->Write(blob_column, row));
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Append(
+ reinterpret_cast<const uint8_t*>(descriptor->data()),
descriptor->size()));
+ }
+ std::shared_ptr<arrow::Array> descriptor_column;
+
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Finish(&descriptor_column));
+ new_children[field_index] = std::move(descriptor_column);
+ }
+
+ auto struct_type =
checked_pointer_cast<arrow::StructType>(value_type_);
+ std::shared_ptr<arrow::StructArray> externalized_array;
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ externalized_array, arrow::StructArray::Make(new_children,
struct_type->fields()));
+ ArrowArray c_array;
+
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*externalized_array,
&c_array));
+ return std::make_unique<RecordBatch>(batch->GetPartition(),
batch->GetBucket(), row_kinds,
+ &c_array);
+ }();
+ if (!result.ok()) {
Review Comment:
`Abort()` deletes every successfully written entry in `uncommitted_packs_`,
even though the owning writer may already hold buffered or pending data that
references those packs. Neither writer is poisoned after the error, so a later
`PrepareCommit` can commit dangling descriptors; the same issue occurs after a
seal failure because `new_files_` has already been flushed. The failure path
must either roll back the pending data and sidecars and make the writer
unusable, or delete only packs not referenced by pending files.
##########
src/paimon/core/append/append_compact_coordinator.cpp:
##########
@@ -361,4 +859,210 @@ Result<std::vector<std::shared_ptr<CommitMessage>>>
AppendCompactCoordinator::Ru
core_options, executor, pool);
}
+Result<int32_t> AppendCompactCoordinator::RunAndCommit(
+ const std::string& table_path, const std::map<std::string, std::string>&
options,
+ const std::vector<std::map<std::string, std::string>>& partitions,
+ const std::string& commit_user, const std::shared_ptr<FileSystem>&
file_system,
+ const std::shared_ptr<MemoryPool>& input_pool, int64_t
candidate_files_per_round) {
+ if (commit_user.empty()) {
+ return Status::Invalid("AppendCompactCoordinator::RunAndCommit needs a
commit user.");
+ }
+ if (candidate_files_per_round <= 0) {
+ return Status::Invalid(fmt::format(
+ "candidate_files_per_round must be positive, but was {}.",
candidate_files_per_round));
+ }
+ auto pool = input_pool ? input_pool : GetDefaultPool();
+ std::shared_ptr<Executor> executor = CreateDefaultExecutor();
+ auto logger = Logger::GetLogger("AppendCompactCoordinator");
+
+ std::pair<std::shared_ptr<TableSchema>, CoreOptions> schema_and_options;
+ PAIMON_ASSIGN_OR_RAISE(schema_and_options,
+ LoadSchemaAndOptions(table_path, options,
file_system));
+ const auto& [table_schema, core_options] = schema_and_options;
+ PAIMON_RETURN_NOT_OK(ValidateTable(table_schema, core_options));
+
+ auto arrow_schema =
DataField::ConvertDataFieldsToArrowSchema(table_schema->Fields());
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<arrow::Schema> partition_schema,
+ FieldMapping::GetPartitionSchema(arrow_schema,
table_schema->PartitionKeys()));
+ auto snapshot_manager =
+ std::make_shared<SnapshotManager>(core_options.GetFileSystem(),
table_path);
+ auto schema_manager =
std::make_shared<SchemaManager>(core_options.GetFileSystem(), table_path);
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<FileStorePathFactory> path_factory,
+ BuildPathFactory(table_path, table_schema, arrow_schema, core_options,
pool));
+
+ if (!core_options.DataEvolutionEnabled()) {
+ // A plain append table has no row id space to split on, so it keeps
the single-round
+ // behaviour and only gains the commit.
+ PAIMON_ASSIGN_OR_RAISE(std::vector<std::shared_ptr<CommitMessage>>
messages,
+ Run(table_path, options, partitions,
file_system, pool));
+ if (messages.empty()) {
+ return 0;
+ }
+ PAIMON_RETURN_NOT_OK(
+ CommitRound(table_path, commit_user, core_options, messages,
executor, pool));
+ return 1;
+ }
+ if (core_options.DataEvolutionCompactionRewriteRowIds()) {
+ return Status::Invalid(fmt::format(
+ "'{}' is no longer supported: normal data-evolution compaction
preserves row ids "
+ "and logical deletions.",
+ Options::DATA_EVOLUTION_COMPACTION_REWRITE_ROW_IDS));
+ }
+
+ // The windows are planned once, against the snapshot the run starts from;
every round
+ // then re-scans and sees the rounds committed before it. That re-scan
re-reads the
+ // snapshot and the manifest list, but not most of the manifest files: a
manifest is
+ // pruned by its recorded row id range, and because windows are cut only
at coverage gaps
+ // it belongs to exactly one round.
`DataEvolutionCompactPlanner::PlanRowIdWindows`
+ // documents the one exception, a delete-only manifest without row id
statistics.
+ PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> snapshot,
snapshot_manager->LatestSnapshot());
+ if (!snapshot.has_value()) {
+ return 0;
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<ManifestList> manifest_list,
+ ManifestList::Create(core_options.GetFileSystem(),
core_options.GetManifestFormat(),
+ core_options.GetManifestCompression(),
path_factory,
+ core_options.GetCache(), pool));
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<PredicateFilter> partition_filter,
+ FileStoreScan::CreatePartitionPredicate(
+ table_schema->PartitionKeys(),
+ core_options.GetPartitionDefaultName(),
arrow_schema, partitions));
+ PAIMON_ASSIGN_OR_RAISE(std::vector<Range> windows,
+ DataEvolutionCompactPlanner::PlanRowIdWindows(
+ manifest_list, snapshot.value(),
partition_filter, partition_schema,
+ candidate_files_per_round, logger.get()));
+ if (windows.empty()) {
+ // Nothing to split on; one unbounded round still commits correctly.
+ windows.emplace_back(0, std::numeric_limits<int64_t>::max());
+ }
+ PAIMON_LOG_DEBUG(logger, "Compacting table %s in %zu round(s)",
table_path.c_str(),
+ windows.size());
+
+ int32_t committed_rounds = 0;
+ for (const auto& window : windows) {
+ PAIMON_ASSIGN_OR_RAISE(
+ std::vector<std::shared_ptr<CommitMessage>> messages,
+ RunDataEvolutionRound(table_path, snapshot_manager,
schema_manager, table_schema,
+ arrow_schema, partition_schema,
core_options, path_factory,
+ partitions, window, executor, pool));
+ if (messages.empty()) {
+ continue;
+ }
+ Status commit_status =
+ CommitRound(table_path, commit_user, core_options, messages,
executor, pool);
+ if (!commit_status.ok()) {
+ // The round's rewritten data files are unreachable once its
commit failed, and the
+ // rounds already committed stay committed: compaction is
idempotent, a later run
+ // re-plans whatever is left. A deletion-vector index file the
round may have
+ // written is left to orphan cleaning, which is what collects
unreferenced index
+ // files.
+ CleanupCompactOutputs(messages, path_factory, core_options);
Review Comment:
`FileStoreCommitImpl::TryCommitOnce` deliberately treats an atomic
snapshot-write error as an uncertain result and keeps outputs for recovery via
`FilterAndCommit`. This branch instead deletes every `CompactAfter` on any
commit error; if the snapshot was committed but the client timed out, the
latest snapshot now references missing data files. Give each round a
recoverable unique identifier and delete outputs only after confirming the
commit did not land; otherwise keep them.
##########
src/paimon/core/operation/commit/conflict_detection.cpp:
##########
@@ -434,11 +801,76 @@ Status ConflictDetection::CheckKeyRange(const
std::vector<ManifestEntry>& merged
Status ConflictDetection::CheckRowIdExistence(const
std::vector<ManifestEntry>& base_entries,
const
std::vector<ManifestEntry>& delta_entries,
- const std::optional<int64_t>&
next_row_id) const {
+ const std::optional<int64_t>&
next_row_id,
+ const Snapshot::CommitKind&
commit_kind) const {
if (!options_.DataEvolutionEnabled()) {
return Status::OK();
}
+ std::vector<const ManifestEntry*> existing_data_files;
Review Comment:
This path does not first verify that a delta DELETE still matches the
current file with the same `Identifier` by `first_row_id` and `row_count`.
Because those fields are not part of `Identifier`, a stale DELETE can cancel a
concurrently reassigned ADD; the materialization output has no row ID and
therefore skips the later range check, potentially replacing current data with
a stale rewrite. Compare the base ADD and delta DELETE metadata first and
reject the commit on any mismatch.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]