Skip to content

feat: support merge append action #902

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

ZENOTME
Copy link
Contributor

@ZENOTME ZENOTME commented Jan 20, 2025

This PR complete #736

@ZENOTME
Copy link
Contributor Author

ZENOTME commented Jan 20, 2025

cc @Fokko @liurenjie1024 @Xuanwo @sdd

@@ -1182,6 +1182,12 @@ impl ManifestEntry {
pub fn data_file(&self) -> &DataFile {
&self.data_file
}

/// get file sequence number
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// get file sequence number
/// File sequence number indicating when the file was added. Inherited when null and status is 1 (added).

from https://iceberg.apache.org/spec/#manifests

Copy link
Contributor

@jonathanc-n jonathanc-n left a comment

Choose a reason for hiding this comment

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

Overall just some small nits. implementing the defaults values for new fields can be done after this pr (#737)

// Enable merge append for table
let tx = Transaction::new(&table);
table = tx
.set_properties(HashMap::from([
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also try adding a test here for the MANIFEST_TARGET_SIZE_BYTES property?

/// Finished building the action and apply it to the transaction.
pub async fn apply(self) -> Result<Transaction<'a>> {
if self.merge_enabled {
let process = MergeManifsetProcess {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let process = MergeManifsetProcess {
let process = MergeManifestProcess {

}
}

impl ManifestProcess for MergeManifsetProcess {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
impl ManifestProcess for MergeManifsetProcess {
impl ManifestProcess for MergeManifestProcess {

Comment on lines 388 to 393
struct MergeManifsetProcess {
target_size_bytes: u32,
min_count_to_merge: u32,
}

impl MergeManifsetProcess {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
struct MergeManifsetProcess {
target_size_bytes: u32,
min_count_to_merge: u32,
}
impl MergeManifsetProcess {
struct MergeManifestProcess {
target_size_bytes: u32,
min_count_to_merge: u32,
}
impl MergeManifestProcess {

Comment on lines 419 to 420
for manifset_file in manifest_bin {
let manifest_file = manifset_file.load_manifest(&file_io).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
for manifset_file in manifest_bin {
let manifest_file = manifset_file.load_manifest(&file_io).await?;
for manifest_file in manifest_bin {
let manifest_file = manifest_file.load_manifest(&file_io).await?;

Ok(merged_bins.into_iter().flatten().collect())
}

async fn merge_manifeset<'a>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
async fn merge_manifeset<'a>(
async fn merge_manifest<'a>(

@@ -267,13 +376,174 @@ trait SnapshotProduceOperation: Send + Sync {
struct DefaultManifestProcess;

impl ManifestProcess for DefaultManifestProcess {
fn process_manifeset(&self, manifests: Vec<ManifestFile>) -> Vec<ManifestFile> {
manifests
async fn process_manifeset<'a>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
async fn process_manifeset<'a>(
async fn process_manifest<'a>(

}
}

trait ManifestProcess: Send + Sync {
fn process_manifeset(&self, manifests: Vec<ManifestFile>) -> Vec<ManifestFile>;
fn process_manifeset<'a>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
fn process_manifeset<'a>(
fn process_manifest<'a>(

}

impl ManifestProcess for MergeManifsetProcess {
async fn process_manifeset<'a>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
async fn process_manifeset<'a>(
async fn process_manifest<'a>(

return Ok(manifests);
}

let first_manifest = manifests[0].clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this be an expensive clone (not wrapped in arc)? is there a way to avoid?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

async fn merge_group<'a>(
        &self,
        snapshot_produce: &mut SnapshotProduceAction<'a>,
        first_manifest: &ManifestFile,
        group_manifests: Vec<ManifestFile>,
    )

This function will take ownership of the manifests, so we have to clone the first one out. Looks like hard to avoid it. Welcome for any suggestion to avoid this.

@ZENOTME
Copy link
Contributor Author

ZENOTME commented Mar 13, 2025

Thanks for review! @jonathanc-n @kevinjqliu. I have refined the code and fixed the test. I think this PR is ready to review again. also cc @Fokko @liurenjie1024 @Xuanwo @sdd

// For this first manifest, it will be pack with the first additional manifest and
// the count(2) is less than the min merge count(4), so these two will not merge.
// See detail: `MergeManifestProcess::merge_group`
if idx == 0 {
Copy link
Contributor Author

@ZENOTME ZENOTME Mar 13, 2025

Choose a reason for hiding this comment

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

I'm not sure whether it's expected behaviour. This manifest will not be merged, so the status will remain Added. cc @Fokko @kevinjqliu

liurenjie1024 pushed a commit that referenced this pull request Jul 2, 2025
## Which issue does this PR close?


This PR add new_manifest_writer in SnapshotProducer and this function
can be used to create different manifset writer for different action in
the future, e.g. MergeAppend
#902

## What changes are included in this PR?



## Are these changes tested?



Co-authored-by: ZENOTME <[email protected]>
@ZENOTME ZENOTME force-pushed the merge_append_new branch from 69e6a40 to 261cbfd Compare July 14, 2025 14:44
@ZENOTME ZENOTME force-pushed the merge_append_new branch from 261cbfd to cc00f8e Compare July 14, 2025 14:52
@ZENOTME
Copy link
Contributor Author

ZENOTME commented Jul 14, 2025

I have fix this PR to adapt new transcaction framework and ready to review. Recently I will keep up this PR. cc @liurenjie1024 @Xuanwo @Fokko @kevinjqliu @CTTY @jonathanc-n

Copy link
Contributor

@CTTY CTTY left a comment

Choose a reason for hiding this comment

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

Hi @ZENOTME , thanks for this awesome work! LGTM overall, I've left some questions in the comments

.metadata()
.properties()
.get(MANIFEST_TARGET_SIZE_BYTES)
.and_then(|s| s.parse().ok())
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should expose the parse error here

If users set MANIFEST_TARGET_SIZE_BYTES=1024+ by mistake, ok() here will silently fall back to the default value. And it will be very hard to find out what happened

I've used something like below instead in another PR:

let total_delay = match props.get(PROPERTY_COMMIT_TOTAL_RETRY_TIME_MS) {
            Some(value_str) => value_str.parse::<u64>().map_err(|e| {
                Error::new(
                    ErrorKind::DataInvalid,
                    "Invalid value for commit.retry.total-timeout-ms",
                )
                .with_source(e)
            })?,
            None => PROPERTY_COMMIT_TOTAL_RETRY_TIME_MS_DEFAULT,
        };

It would be so much easier if we had something like a PropertyUtil to define the orthogonal way of getting and parsing properties

@@ -108,7 +108,7 @@ impl TransactionAction for FastAppendAction {
}
}

struct FastAppendOperation;
pub(crate) struct FastAppendOperation;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Maybe we should just put MergeAppendAction under append.rs as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That will make append.rs file too big I think.


if self.merge_enabled {
snapshot_producer
.commit(FastAppendOperation, MergeManifsetProcess {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: should we change FastAppendOperation to AppendOperation?

.merge_manifest(snapshot_produce, unmerg_data_manifests)
.await?
};
data_manifests.extend(unmerge_delete_manifest);
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry I'm a bit confused while reading this, could you please help me understand:

  • why do we care about delete_manifest? I think MergeAppend should not introduce delete manifest but maybe I'm wrong
  • Why should we leave delete_manifest unmerged?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why do we care about delete_manifest? I think MergeAppend should not introduce delete manifest but maybe I'm wrong

Yes, we will not process delete manifest. In here, we:

  1. fitler out all data manifest and merge them
  2. keep delete manifest but not process them (We can't drop them)
  3. concat processed data manifest and delete manifest and return them

Why should we leave delete_manifest unmerged?

This implementation refer from pyiceberg: https://github.com/apache/iceberg-python/blob/e9c025318787bfd34b98a3fc41544e0f168904ba/pyiceberg/table/update/snapshot.py#L553. I guess the reason is that in MergeAppend we only append data file, so we assume the delete file will not change so we don't need to process them.

But I notice that here is different from iceberg-java. In iceberg-java, merge append action use MergeSnapshotProducer. And MergeSnapshotProducer will merge both data manifest and delete manifest.

@@ -40,3 +40,148 @@ pub(crate) fn available_parallelism() -> NonZeroUsize {
NonZeroUsize::new(DEFAULT_PARALLELISM).unwrap()
})
}

pub mod bin {
Copy link
Contributor

Choose a reason for hiding this comment

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

+1, an iterable packer with lookback would be more memory efficient. This can be completed as a follow-up

pub fn file_sequence_number(&self) -> Option<i64> {
self.file_sequence_number
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see this used anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In merge_append_test.rs

- rename FastAppendOperation to AppendOperation
- use iterable input for pack to be more memory efficient
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants