Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Storages/IStorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ struct StreamLocalLimits;
class EnabledQuota;
struct SelectQueryInfo;

/// Declared opaquely (definition in Core/SettingsEnums.h) to keep this widely included header light.
enum class MergeTreePartExportFileAlreadyExistsPolicy : uint8_t;

using NameDependencies = std::unordered_map<String, std::vector<String>>;
using DatabaseAndTableName = std::pair<String, String>;

Expand Down Expand Up @@ -459,7 +462,7 @@ It is currently only implemented in StorageObjectStorage.
const std::string & /* file_name */,
Block & /* block_with_partition_values */,
const std::function<void(const std::string &)> & /* new_file_path_callback */,
bool /* overwrite_if_exists */,
MergeTreePartExportFileAlreadyExistsPolicy /* file_already_exists_policy */,
std::size_t /* max_bytes_per_file */,
std::size_t /* max_rows_per_file */,
const std::optional<std::string> & /* iceberg_metadata_json_string */,
Expand Down
2 changes: 1 addition & 1 deletion src/Storages/MergeTree/ExportPartTask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ bool ExportPartTask::executeStep()
filename,
block_with_partition_values,
new_file_path_callback,
manifest.file_already_exists_policy == MergeTreePartExportManifest::FileAlreadyExistsPolicy::overwrite,
manifest.file_already_exists_policy,
manifest.settings[Setting::export_merge_tree_part_max_bytes_per_file],
manifest.settings[Setting::export_merge_tree_part_max_rows_per_file],
manifest.iceberg_metadata_json,
Expand Down
78 changes: 67 additions & 11 deletions src/Storages/ObjectStorage/MultiFileStorageObjectStorageSink.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include <Storages/ObjectStorage/MultiFileStorageObjectStorageSink.h>
#include <Interpreters/Context.h>
#include <Common/logger_useful.h>
#include <IO/ReadBufferFromFileBase.h>
#include <IO/ReadHelpers.h>
#include <filesystem>

namespace DB
Expand All @@ -9,6 +11,16 @@ namespace DB
namespace ErrorCodes
{
extern const int FILE_ALREADY_EXISTS;
extern const int CORRUPTED_DATA;
}

namespace
{
/// The commit file lives in the same directory as the data files.
std::string commitFilePath(const std::string & base_path, const String & transaction_id)
{
return (std::filesystem::path(base_path).parent_path() / ("commit_" + transaction_id)).string();
}
}

MultiFileStorageObjectStorageSink::MultiFileStorageObjectStorageSink(
Expand All @@ -18,24 +30,49 @@ MultiFileStorageObjectStorageSink::MultiFileStorageObjectStorageSink(
StorageObjectStorageConfigurationPtr configuration_,
std::size_t max_bytes_per_file_,
std::size_t max_rows_per_file_,
bool overwrite_if_exists_,
FileAlreadyExistsPolicy file_already_exists_policy_,
const std::function<void(const std::string &)> & new_file_path_callback_,
const std::optional<FormatSettings> & format_settings_,
SharedHeader sample_block_,
ContextPtr context_)
: SinkToStorage(sample_block_),
base_path(base_path_),
transaction_id(transaction_id_),
commit_file_path(commitFilePath(base_path_, transaction_id_)),
object_storage(object_storage_),
configuration(configuration_),
max_bytes_per_file(max_bytes_per_file_),
max_rows_per_file(max_rows_per_file_),
overwrite_if_exists(overwrite_if_exists_),
file_already_exists_policy(file_already_exists_policy_),
new_file_path_callback(new_file_path_callback_),
format_settings(format_settings_),
sample_block(sample_block_),
context(context_)
{
if (file_already_exists_policy != FileAlreadyExistsPolicy::overwrite)
{
if (auto committed_paths = tryReadCommittedPaths())
{
Comment on lines +54 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify committed objects before accepting the marker

When a commit marker survives but one of its referenced data objects has been deleted or expired, this branch reports every recorded path and turns the resulting FILE_ALREADY_EXISTS into a successful skip export without checking that those objects still exist. For the common single-file export, the previous path-level existence check would instead rewrite a missing object; this change can now mark the export COMPLETED and publish a path that returns no data. Validate every committed path before accepting the marker, and reconstruct or fail if any are missing.

Useful? React with 👍 / 👎.

if (committed_paths->empty())
throw Exception(ErrorCodes::CORRUPTED_DATA,
"Commit file {} lists no data files", commit_file_path);

/// Report the whole committed set before throwing: a caller applying `skip` takes these
/// paths as the part's export result, so it needs every file and not just the first.
for (const auto & committed_path : *committed_paths)
new_file_path_callback(committed_path);

throw Exception(ErrorCodes::FILE_ALREADY_EXISTS,
"Part was already exported as {} file(s), see commit file {}",
committed_paths->size(), commit_file_path);
}
}

/// No commit file: either a fresh export, or an attempt that died before finalizing every
/// file. `error` still reports the leftovers as a conflict, but `skip` has to rewrite them --
/// the files that attempt never reached carry rows no later attempt produces.
overwrite_data_files = file_already_exists_policy != FileAlreadyExistsPolicy::error;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve existing objects under the skip policy

When no commit marker exists but a generated data path already exists—for example, an independently uploaded object, a removed marker, or an older incomplete export—skip now sets overwrite_data_files and silently rewrites that object. The documented policy is to skip an existing file, and absence of the marker does not establish ownership of the existing object, so this fallback can destructively replace unrelated data; fail closed on such a conflict unless ownership can be proven.

AGENTS.md reference: AGENTS.md:L153-L153

Useful? React with 👍 / 👎.


current_sink = createNewSink();
}

Expand Down Expand Up @@ -72,15 +109,13 @@ std::shared_ptr<StorageObjectStorageSink> MultiFileStorageObjectStorageSink::cre
{
auto new_path = generateNewFilePath();

/// todo
/// sounds like bad design, but callers might decide to ignore the exception, and if we throw it before the callback
/// they will not be able to grab the file path.
/// maybe I should consider moving the file already exists policy in here?
/// The callback runs before the conflict check on purpose: under `error` the caller discards
/// the reported path along with the failure, and under the other policies this check is off.
new_file_path_callback(new_path);

file_paths.emplace_back(std::move(new_path));

if (!overwrite_if_exists && object_storage->exists(StoredObject(file_paths.back())))
if (!overwrite_data_files && object_storage->exists(StoredObject(file_paths.back())))
{
throw Exception(ErrorCodes::FILE_ALREADY_EXISTS, "File {} already exists", file_paths.back());
}
Expand Down Expand Up @@ -125,12 +160,33 @@ void MultiFileStorageObjectStorageSink::onFinish()
commit();
}

void MultiFileStorageObjectStorageSink::commit()
std::optional<std::vector<std::string>> MultiFileStorageObjectStorageSink::tryReadCommittedPaths() const
{
/// the commit file path should be in the same directory as the data files
const auto commit_file_path = fs::path(base_path).parent_path() / ("commit_" + transaction_id);
if (!object_storage->exists(StoredObject(commit_file_path)))
return {};

auto in = object_storage->readObject(StoredObject(commit_file_path), context->getReadSettings());

std::vector<std::string> committed_paths;
while (!in->eof())
{
String committed_path;
readStringUntilNewlineInto(committed_path, *in);
if (!in->eof())
in->ignore(1);
if (!committed_path.empty())
committed_paths.emplace_back(std::move(committed_path));
}

if (!overwrite_if_exists && object_storage->exists(StoredObject(commit_file_path)))
return committed_paths;
}

void MultiFileStorageObjectStorageSink::commit()
{
/// The constructor already ruled out a pre-existing commit file for every policy but
/// `overwrite`, so seeing one here means another exporter committed this part while we wrote.
if (file_already_exists_policy != FileAlreadyExistsPolicy::overwrite
&& object_storage->exists(StoredObject(commit_file_path)))
{
throw Exception(ErrorCodes::FILE_ALREADY_EXISTS, "Commit file {} already exists, aborting {} export", commit_file_path, transaction_id);
}
Expand Down
15 changes: 13 additions & 2 deletions src/Storages/ObjectStorage/MultiFileStorageObjectStorageSink.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <Core/SettingsEnums.h>
#include <Storages/ObjectStorage/StorageObjectStorageSink.h>

namespace DB
Expand All @@ -11,14 +12,16 @@ namespace DB
class MultiFileStorageObjectStorageSink : public SinkToStorage
{
public:
using FileAlreadyExistsPolicy = MergeTreePartExportFileAlreadyExistsPolicy;

MultiFileStorageObjectStorageSink(
const std::string & base_path_,
const String & transaction_id_,
ObjectStoragePtr object_storage_,
StorageObjectStorageConfigurationPtr configuration_,
std::size_t max_bytes_per_file_,
std::size_t max_rows_per_file_,
bool overwrite_if_exists_,
FileAlreadyExistsPolicy file_already_exists_policy_,
const std::function<void(const std::string &)> & new_file_path_callback_,
const std::optional<FormatSettings> & format_settings_,
SharedHeader sample_block_,
Expand All @@ -35,11 +38,17 @@ class MultiFileStorageObjectStorageSink : public SinkToStorage
private:
const std::string base_path;
const String transaction_id;
/// Written by `commit` only after every data file has been finalized, so its presence --
/// unlike that of any individual data file -- proves a previous export of this part
/// produced the whole set.
const std::string commit_file_path;
ObjectStoragePtr object_storage;
StorageObjectStorageConfigurationPtr configuration;
std::size_t max_bytes_per_file;
std::size_t max_rows_per_file;
bool overwrite_if_exists;
FileAlreadyExistsPolicy file_already_exists_policy;
/// Data files left behind by an attempt that never reached `commit` have to be rewritten.
bool overwrite_data_files = false;
std::function<void(const std::string &)> new_file_path_callback;
const std::optional<FormatSettings> format_settings;
SharedHeader sample_block;
Expand All @@ -51,6 +60,8 @@ class MultiFileStorageObjectStorageSink : public SinkToStorage

std::string generateNewFilePath();
std::shared_ptr<StorageObjectStorageSink> createNewSink();
/// The data files a previous export of this part committed, or nothing when it never committed.
std::optional<std::vector<std::string>> tryReadCommittedPaths() const;
void commit();
};

Expand Down
4 changes: 2 additions & 2 deletions src/Storages/ObjectStorage/StorageObjectStorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ SinkToStoragePtr StorageObjectStorage::import(
const std::string & file_name,
Block & block_with_partition_values,
const std::function<void(const std::string &)> & new_file_path_callback,
bool overwrite_if_exists,
MergeTreePartExportFileAlreadyExistsPolicy file_already_exists_policy,
std::size_t max_bytes_per_file,
std::size_t max_rows_per_file,
const std::optional<std::string> & iceberg_metadata_json_string,
Expand Down Expand Up @@ -733,7 +733,7 @@ SinkToStoragePtr StorageObjectStorage::import(
configuration,
max_bytes_per_file,
max_rows_per_file,
overwrite_if_exists,
file_already_exists_policy,
new_file_path_callback,
format_settings_ ? format_settings_ : format_settings,
std::make_shared<const Block>(metadata_snapshot->getSampleBlock()),
Expand Down
2 changes: 1 addition & 1 deletion src/Storages/ObjectStorage/StorageObjectStorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class StorageObjectStorage : public IStorage, public IBackgroundOperation
const std::string & /* file_name */,
Block & /* block_with_partition_values */,
const std::function<void(const std::string &)> & new_file_path_callback,
bool /* overwrite_if_exists */,
MergeTreePartExportFileAlreadyExistsPolicy /* file_already_exists_policy */,
std::size_t /* max_bytes_per_file */,
std::size_t /* max_rows_per_file */,
const std::optional<std::string> & /* iceberg_metadata_json_string */,
Expand Down
6 changes: 3 additions & 3 deletions src/Storages/ObjectStorage/StorageObjectStorageCluster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1114,7 +1114,7 @@ SinkToStoragePtr StorageObjectStorageCluster::import(
const std::string & file_name,
Block & block_with_partition_values,
const std::function<void(const std::string &)> & new_file_path_callback,
bool overwrite_if_exists,
MergeTreePartExportFileAlreadyExistsPolicy file_already_exists_policy,
std::size_t max_bytes_per_file,
std::size_t max_rows_per_file,
const std::optional<std::string> & iceberg_metadata_json_string,
Expand All @@ -1126,7 +1126,7 @@ SinkToStoragePtr StorageObjectStorageCluster::import(
file_name,
block_with_partition_values,
new_file_path_callback,
overwrite_if_exists,
file_already_exists_policy,
max_bytes_per_file,
max_rows_per_file,
iceberg_metadata_json_string,
Expand All @@ -1136,7 +1136,7 @@ SinkToStoragePtr StorageObjectStorageCluster::import(
file_name,
block_with_partition_values,
new_file_path_callback,
overwrite_if_exists,
file_already_exists_policy,
max_bytes_per_file,
max_rows_per_file,
iceberg_metadata_json_string,
Expand Down
2 changes: 1 addition & 1 deletion src/Storages/ObjectStorage/StorageObjectStorageCluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class StorageObjectStorageCluster : public IStorageCluster
const std::string & file_name,
Block & block_with_partition_values,
const std::function<void(const std::string &)> & new_file_path_callback,
bool overwrite_if_exists,
MergeTreePartExportFileAlreadyExistsPolicy file_already_exists_policy,
std::size_t max_bytes_per_file,
std::size_t max_rows_per_file,
const std::optional<std::string> & iceberg_metadata_json_string,
Expand Down
Loading
Loading