-
Notifications
You must be signed in to change notification settings - Fork 194
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
init writer framework #135
Conversation
I would suggest to have a class hierarchy to demonstrate the whole picture of this design. |
I draw a diagram to demonstrate the design of writer. Please let me know if it still has some confused parts. For base file writer, they are the base format in iceberg, like: data file, position delete file, equality delete file. They take a file writer by which they write their content into a physical format(parquet, orc, ...). For functional writer, they are the higher level writer to provide more complex write logic. E.g. FanoutPartitionWriter. It act like https://github.com/apache/iceberg/blob/cbb50bfa5ad8cd490e991ff4e1d7bf7c025e3d5d/core/src/main/java/org/apache/iceberg/io/PartitionedFanoutWriter.java#L28. And it can be combined with other iceberg writer. base file writer and functional writer is only distinguished logically. Both them are |
You can find how to write class diagram uml here: https://plantuml.com/class-diagram |
I think this uml diagram is clear enough to demonstrate the architecture of writers. We have applied same design in icelake, and it works well. Looking forward to hear you thoughts? cc @Fokko @Xuanwo |
|
||
/// The current file status of iceberg writer. It implement for the writer which write a single | ||
/// file. | ||
pub trait CurrentFileStatus { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we introduce a separate trait for this, or would it be better to add a status(&self) -> FileStatus
method?
Also, implementing APIs that require &self
complicates making our type Sync
, as it necessitates additional Arc<Mutex>
wrappers. Could we have them accept &mut self
instead? We don't have any use cases for browsing files, do we?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we introduce a separate trait for this, or would it be better to add a status(&self) -> FileStatus method?
The reason e introduce it as a separate trait is that for some writer which may write multiple file at once, so we should not call this trait for them. E.g fanout partition writer.
Could we have them accept &mut self instead?
This interface is read-only. Make it &mut self
seems weird for me.🤔 And I wonder in which case the user want to use Arc<Writer>
? Seems it's a rare example for me.
crates/iceberg/src/writer/mod.rs
Outdated
} | ||
|
||
/// The write result of iceberg writer. | ||
pub trait IcebergWriteResult: Send + Sync + 'static { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that the result only reveals a set_xxx
API is somewhat confusing. How are users supposed to utilize these APIs?
// 4. Create a iceberg writer.
let iceberg_writer = fanout_partition_writer_builder.build(schema).await?;
iceberg_writer.write(input).await?;
let write_result = iceberg_writer.flush().await?;
How write_result
will be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, user can see the type of write_result directly and they can call write_result.build() to get the DataFile.
We also can expose a build interface to make it clear like:
trait IcebergWriteResult {
fn build(self) -> Vec<DataFile>
}
But I'm rethinking do we really need this abstract🤔
We use generic param here because we can have DeltaResult for delta writer like
struct DeltaResult {
data_file: Vec<DataFileBuilder>,
pos_delete_file: Vec<DataFileBuilder>,
eq_delete_file: Vec<DataFileBuilder>
}
But I find that it actually can read the type from DataFile directly. So return Vec<DataFileBuilder>
also can work.
So I think may be we can also discard the IcebergWriteResult, and directly use write(&mut self, input: I) -> Vec<DataFileBuilder>
to make interface more simple. Because at less for now, in iceberg the return result of writer is always DataFile. I'm not sure should we preserve the extendable for it.
/// The associated iceberg write result type. | ||
type R: IcebergWriteResult; | ||
/// Convert to iceberg write result. | ||
fn to_iceberg_result(self) -> Self::R; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What transform will we do in this trait? Can we return IcebergWriteResult
directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I find that we can use IcebergWriteResult directly.🥵 Good point!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I also think the FileWriteResult
/IcebergWriterResult
is a little too complicated. What FileWriter
returns is just a partial DataFile
, so a DataFileBuilder
would be enough.
/// The associated file write result type. | ||
type R: FileWriteResult; | ||
/// Write record batch to file. | ||
async fn write(&mut self, batch: &RecordBatch) -> Result<()>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the relation of RecordBatch
with the I
in IcebergWriter<I>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason why we use I
in IcbergWriter<I>
is that for some writers it needs some specific input format rather than RecordBatch. E.g. the PositionDeleteWriter needs a write interface like write(&mut self, file: &str, position: usize)
. To support this case, we make IcebergWriter<I>
.
But all IcebergWriter will write the data into a file using FileWriter finally. In this time, IcebergWriter should convert the data into a RecordBatch and write them using FileWriter.
E.g. for PositionDeleteWriter, we will use write(&mut self, file: &str, position: usize)
, and PositionDeleteWriter will batch these data like
---
file, position,
file, position,
...
---
, and when it accumulates enough data, it can convert all data into RecordBatch and write them using FileWriter.
For me, RecordBatch is a physical data representation, and I
is like a logical representation. For data file writer, the logical representation and physical representation is the same. But position delete writer, they are different.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should simplify the write result layer, and only use DataFileBuilder
would be enough. Others LGTM
/// The associated iceberg write result type. | ||
type R: IcebergWriteResult; | ||
/// Convert to iceberg write result. | ||
fn to_iceberg_result(self) -> Self::R; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I also think the FileWriteResult
/IcebergWriterResult
is a little too complicated. What FileWriter
returns is just a partial DataFile
, so a DataFileBuilder
would be enough.
To make the design more simple, I have removed the **WriteResult trait. Maybe it's time to mark it as ready for review.🤔 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally LGTM.
use arrow_schema::SchemaRef; | ||
|
||
/// File writer builder trait. | ||
#[async_trait::async_trait] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove this now? We have upgraded to 1.75
crates/iceberg/src/writer/mod.rs
Outdated
type DefaultInput = RecordBatch; | ||
|
||
/// The builder for iceberg writer. | ||
#[async_trait::async_trait] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
crates/iceberg/src/writer/mod.rs
Outdated
|
||
/// The builder for iceberg writer. | ||
#[async_trait::async_trait] | ||
pub trait IcebergWriterBuilder<I = DefaultInput>: Send + Clone + 'static { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
crates/iceberg/src/writer/mod.rs
Outdated
/// Write data to iceberg table. | ||
async fn write(&mut self, input: I) -> Result<()>; | ||
/// Flush the writer and return the write result. | ||
async fn flush(&mut self) -> Result<Vec<DataFileBuilder>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the Vec<DataFileBuilder>
maybe not type safe. But I also don't think we need a common trait for IcebergWriterResult
. How about make IcebergWriter<I, R>
, where R
is its return type? The reason I propose this is that, when IcebergWriter
is used, the caller usually know its concrete type, and the most useful use case of this abstraction is that we can have different layers for the writer, which should not change it's input and return type.
* remove async trait
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
Hi, I find that we can modify
as
The following traits can also help export metrics in the future. |
Trait is usually used to abstract things and hide concrete implementation. I don't quite understand the goal of this trait |
Sorry, Let me try to illustrate more clearly. Let me use a example to illustrate it:
And through
Do we have another way?Yes, we also can do like this:
But I feel that all these traits can be abstract as |
This design is kind of too complicated for me. I still prefer the approach we used in icelake, and don't overuse too much traits for just saving code. |
Actually, this design is to solve a problem I meet in icelake now.🤔 In icelake, we use an interface like the following:
And we have a corresponding monitor writer:
But after I introduced a new partition writer: |
These kinds of refactoring helps avoiding some code duplication, but I don't think this is good abstraction. Good abstraction should be clean and easy to understand. Even your example is not quite convincible, for example, what if the metrics of these two partition writers diverge someday in future, e.g. I want to count the latency of computing these partition calculation? |
Sounds reasonable. Let's use the approach before now. We can solve code duplication when it has a good abstraction. |
use arrow_schema::SchemaRef; | ||
|
||
/// File writer builder trait. | ||
#[allow(async_fn_in_trait)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to use impl Future
in trait so that we can make sure the future is Send
:
fn build(self, schema: &SchemaRef) -> impl Future<Output=Result<Self::R>> + Send;
I feel that for this writer framework, we may need more discussion, so I can separate this framework as And we can work on the |
Let's mark this pr as draft? cc @ZENOTME |
I found a new way to make this API can be used like the following:
This may make this interface easier to make sense.🤔 Also feel free to leave any suggestions or confusion, I'm glad to improve it to be easier to understand and use. |
The real code could be like: let writer = FileWriterHelper::new(builder_a)
.layer(builder_b)
.layer(builder_c)
.finish(builder_d)
.layer(builder_e)
.layer(builder_f)
.build()
.await
.unwrap(); Which is harder to understand. The original design like the following is verbose but much easier to read even when we not designed the var names carefully. let builder = ParquetFileWriterBuilder::new(parquet_file_writer_config);
let builder = DataFileWriterBuilder::new(builder,data_file_writer_config);
let writer = builder.build(schema).await?; |
Thanks for the feedback. Let's use the original design. It's just an experiment🤣. |
close by #275 |
related issue: #34
I drafted a writer framework which has been implemented in icelake icelake-io/icelake#243 and proved that it's extensible and flexible. The following is the introduction of this API:
Target
The writer API is designed to be extensible and flexible. Each writer is decoupled and can be create and config independently. User can:
can process the data in a specific way.
How it works
There are two kinds of writer and related builder:
IcebergWriter
andIcebergWriterBuilder
, they are focus on the data process logical.If you want to support a new data process logical, you need to implement a new
IcebergWriter
andIcebergWriterBuilder
.FileWriter
andFileWriterBuilder
, they are focus on the physical file write.If you want to support a new physical file format, you need to implement a new
FileWriter
andFileWriterBuilder
.The create process of iceberg writer is:
FileWriterBuilder
.1a. Combine it with other
FileWriterBuilder
to get a newFileWriterBuilder
.IcebergWriterBuilder
.2a. Combine it with other
IcebergWriterBuilder
to get a newIcebergWriterBuilder
.build
function inIcebergWriterBuilder
to create aIcebergWriter
.Simple Case 1: Create a data file writer using parquet file format.
Complex Case 2: Create a fanout partition data file writer using parquet file format.
More case: may be the example in icelake
https://github.com/icelake-io/icelake/blob/949dda79d2ebdfa7ad07e2f88a67d01d7040c181/icelake/tests/insert_tests_v2.rs#L164
Feel free for any suggestions and I'm glad to modify them.