-
Notifications
You must be signed in to change notification settings - Fork 166
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
Consider Using object_store as IO Abstraction #172
Comments
Hi, @tustvold @alamb Thanks for this proposal and write up, object_store looks great to me! In iceberg's design, all file ios are hidden under the Currently
I'm quite interested in this since we are about to add support for file reader/writer, which will heavily depend on |
cc @Xuanwo |
Hi @tustvold, thank you for initiating this discussion! I will do my best to offer a multifaceted response with different hat. Put iceberg-rust developers hat onAs @liurenjie1024 mentioned, iceberg-rust features its own It's fine to integrate with Here are some remarks regarding the object_store feature set:
iceberg-rust is aligned with Iceberg and PyIceberg, sharing the same configuration logic; therefore, the object_store's configuration system is redundant for our purposes.
While the conditional put feature offers certain advantages, it may not be as crucial for our current use cases in iceberg-rust, where integration with a catalog like Hive or REST is more common. As an iceberg-rust developer, I am eager to unlock more potential within the project. Put OpenDAL maintianer hat onFirstly, opendal and object_store are not competitors. (And remember, I'm also a contributor to I believe Here are a few reasons why OpenDAL is beneficial for iceberg-rust.
I also found some places that OpenDAL can improve (Thanks @tustvold!):
As an OpenDAL maintainer, I believe OpenDAL offers features that could be beneficial for iceberg-rust, potentially simplifying some aspects of storage management. And I will be happy to collaborate with |
Thank you both for the responses.
Glad to here efforts are being made to keep the IO primitives abstracted and pluggable 👍. I would just observe that FileIO appears to mirror filesystem APIs, and that this has historically been a pain point in systems that chose this path. For example Spark has had a very hard time getting a performant S3 integration, with proper vectored IO only being added to OSS Spark very recently. By contrast the object_store APIs mirror those of the actual stores, and are designed to work well with the APIs in arrow-rs, avoiding all the complexities of prefetching heuristics and similar.
I entirely agree, I guess I was more suggesting that the IO abstraction mirror object_store as this is what both the upstream crates use and expect, and what the underlying stores provide. If people then wanted additional backend support they could plug OpenDAL into this interface?
I'd be happy to help out with this, if you're open to contributions, both myself and my employer are very interested in native iceberg support for the Rust ecosystem |
Thank you all -- this is a great conversation.
I took a look at the FileIO interface that @liurenjie1024 and @Xuanwo pointed it. Eventually they seem to provide something that implements While it is true that The "benefit" that one might get from using In my opinon, the use of OpenDAL to connect to more storage systems other than object stores is pretty compelling. Perhaps as you proceed integrating iceberg-rust with arrow-rs/parquet/datafusion we will learn more about how these various systems can be integrated and if any adjustments need to be made, either in OpenDAL or object_store or downstream in some other crates |
Thanks everyone for this very nice discussion.
Of course we are open to contributions from everyone, and that's the key spirit of open source project. Please note that this is an apache project, and everyone is welcome to contribute. As with the |
If primarily performing sequential IO I would tend to agree, the AsyncRead abstraction will be less efficient than a streaming request, but if pre-fetching is configured appropriately the end-to-end latency should be similar. However, it is "random" IO such as occurs when reading structured file formats like parquet, that this difference becomes more stark. Fortunately the fix is extremely simple, adding
Would you be open to a PR to allow using either OpenDAL or object_store, along with corresponding feature flags, or would you prefer to not complicate matters at this time? I think this could be achieved in a fairly unobtrusive manner. |
Thanks @tustvold for raising this and please don't hesitate to open an issue or PR.
This is why the Iceberg Java implementation ships with its own vectorized parquet reader :) It looks to me that |
That's awesome, thank you for the link. That is exactly what object_store is, an opinionated abstraction that ensures workloads are not overly reliant on filesystem-specific APIs and behaviour. Really cool that the iceberg community chose to take this approach, I agree with it wholeheartedly 👍 FWIW I notice that the InputFile contract is not vectorised itself, but I guess if you have a custom parquet reader you could lift the range coalescing into it. |
Hi, @tustvold Welcome to open pr for this. About the timing, my suggestion is to wait for a moment. Currently this crate has finished rest catalog and serialization/deserialization of metadata, basic file based table scan planning. We are expecting to implement two things following: a parquet file writer which writes arrow record batch, and reading parquet file to arrow record batch stream. These two features depends on |
Hi, I'm a relatively new user of the I've read over the discussion here, as well as #356, reviewed the ethos behind I'm not sure if @tustvold still has the time to open a PR, but I would be happy to help contribute towards this effort as well. |
Hi, thank you, @BlakeOrth, for bringing this up. It's part of our community's philosophy not to choose a winner. All implementations of Iceberg don't directly expose the underlying storage implementations. Instead, we provide a trait (interface) called We have reached a consensus in previous discussions to expose such a trait, allowing users to choose between A similar discussion also occurred on arrow-rs, where users want to use opendal along with |
@BlakeOrth I don't fully understand what you are trying to accomplish. If the goal is to use iceberg-rust without OpenDAL, I thought it was possible to implement an adapter/ wrapper around an Is the issue there is no community maintained adapter? |
At the moment FileIO is not a trait but a struct - https://docs.rs/iceberg/latest/iceberg/io/struct.FileIO.html I personally think it would be valuable to make this a trait so people can bring their own IO implementations, be they based on object_store or something else. This is the approach we have taken elsewhere in the ecosystem and it works well |
To add some context here. As mentioned, The goal behind Another important aspect of the FileIO is that it harmonizes the configuration, so you can easily swap out Edit: @tustvold and I commented at the same time. I agree that it should be a |
Yes, I think it should be a trait as well. Its current form has historical reasons, and I would be happy to refine it into the shape we envisioned. If someone wants to work in this area, feel free to reach out to me through issues/PRs or on Slack. |
By the way, we will need to present |
What about adding a FileIOProvider that constructs an Edit: although that does naturally lead to the question of whether iceberg-rust even really needs to rebuild these abstractions when they exist upstream... |
I agree that we should make |
One potential API that would not cause disruption to the current consumers would be to add a extension trait like we do with UserDefinedLogicalNode in DataFusion The user would create a let my_wrapper = Arc::new(ObjectStoreWrapper::new(...))
...
// FileIO remains a struct that wraps a Storage enum
let file_io = FileIO::from_extension(my_wrapper) We could add a new variant of
A user would create one like struct ObjectStoreWrapper {
...
}
/// New trait that users would have to implement
impl FileIOExtension {
...
} |
The transformation from the current abstraction to an object safe version could be done largely mechanically
This would of course be more disruptive than what @alamb proposes, but would be more extensible and avoid needing to define a secondary extension interface. It would also more closely mirror the java APIs which I understand is a goal here. Edit: That being said if providing an idiomatic API for Rust users is more important than matching the Java APIs I can make alternative suggestions along those lines. |
My two cents: I think it would make sense to list all requirements. The main requirements I can think of are:
If the mentioned requirements are the main motivation, I think it makes sense to use Regarding 2: I think the common iceberg configuration could be handled with something like the existing parse_url_opt that parses a given iceberg configuration. This doesn't require a struct. In my opinion, the rest of the table configuration should be part of the table and not of FileIO. So for me it comes down to, what are the other requirements? And do they prohibit the use of a trait because of object safety. |
+1 for making There's an analogy here with delta-rs, in that it also has its own high-level abstraction called LogStore, with which you can construct the
Perhaps ergonomics can be counted as one? Things like wrapping your FileIO/store/client with a path prefix, listing objects in a given path (I find this useful for debugging sometimes) and doing some more sophisticated stuff such as multipart uploads are all quite useful to us (and they're built into the object_store crate). |
To increase the ergonomics one could introduce an additional trait that would be implemented on top of #[async_trait]
pub trait FileIO {
async fn get_metadata(&self, location: Path) -> Result<TableMetadata, Error>;
}
#[async_trait]
impl<T: ObjectStore> FileIO for T {
async fn get_metadata(&self, location: Path) -> Result<TableMetadata, Error> {
let bytes = self.get(&location).await?.bytes().await?;
Ok(serde_json::from_slice(&bytes)?)
}
} This provides better ergonomics while maintaining all the flexibility that you get with |
Hi, the Using |
Wow, I did not expect my comment digging up a nearly year old issue to result in this much discussion! I likely have less skin in the game than any of the maintainers here, but I think @gruuya and @JanKaul's most recent comments mirror what I personally had in mind. That being said, I think pretty much any of the proposed solutions would allow users to solve the underlying problem here. I would also like to highlight @tustvold's comment:
I personally think having an API that's idiomatic ends up being very important for long-term usability, and in turn the long-term health, of a given package. Maintaining similar units of abstraction, and the names of those units of abstraction, across a multi-language ecosystem such as arrow/iceberg/etc. is important for developer familiarity. However, I have personally seen multiple instances where the community has entirely re-implemented a package because the original API was mirrored from another language and was not ergonomic to use. |
Thanks for everyone joining the discussion here. I think we have reached some conclusions here:
One thing undetermined is about the relationship with java api. The current design tries to be close to java api, but keep idiomatic for rust users. That's why we are not making While both @tustvold and @alamb 's suggestions are great, one of my concerns is breaknig current api. In fact, in current we have enough room for extensions. For example, if we want to use
One missing point point of this design is that we don't allow user to provide external |
Sounds like a good compromise, did you have any thoughts on how this might integrate with the existing Datafusion machinery? I'm mainly thinking for configuration, so users get a good out of box experience, but one could see the benefits of iceberg using more of DFs first party support for things like parquet |
I'm not sure I get your point about the existing Datafusion machinery, do you mean how to pass configuration down to |
DataFusion provides an ObjectStoreRegistry as part of the SessionContext. This is then what various abstractions like ParquetExec hook into. By integrating with this iceberg-rs would better interoperate with the rest of the DataFusion ecosystem, be they other catalogs like listing table, deltalake, Hive, etc... or unusual deployment scenarios with custom caching object stores, etc... It seems unfortunate for users to need to configure iceberg-rs separately from the rest of DataFusion. It would also benefit from the ongoing work to improve those components and systems. I don't know to what extent the desire is to make iceberg-rs a standalone library that mirrors the Java APIs and configuration, but I thought it worthwhile to at least make the case for closer integration with DataFusion. It seems like quite a lot of undifferentiated toil to rebuild the quite subtle logic around predicate pushdown, concurrent decode, etc... Edit: to ground this a bit more, the advantage of a trait based approach, is the DF bindings could provide a component wrapping SessionContext or similar, without forcing iceberg to take a dependency on DF or maintain this mapping |
If we want to allow integrating with #[async_trait]
pub trait StorageProvider {
async fn build(&self, configs: &HashMap<String, String>, url: &str) -> Arc<dyn Stroage>;
} |
I think that should work, the DataFusion wrapper can just hook the iceberg metadata operations into via that StorageProvider trait, and then use the DataFusion machinery directly for the actual file I/O. I presume this would be possible, or does this library expect to also perform the file I/O? |
@liurenjie1024 I have taken some time to explore an implementation based on your suggestion above, just as I did for the user extensible There's a chance I didn't fully grasp your proposed solution, so if this solution isn't what you had in mind, please let me know. Based on your proposal, I changed #[async_trait::async_trait]
pub(crate) trait Storage: Debug {
async fn create_reader(&self, path: &str) -> Result<Arc<dyn FileRead>>;
async fn create_writer(&self, path: &str) -> Result<Arc<dyn FileWrite>>;
async fn metadata(&self, path: &str) -> Result<FileMetadata>;
async fn exists(&self, path: &str) -> Result<bool>;
async fn delete(&self, path: &str) -> Result<()>;
} Ideally I believe #[derive(Clone, Debug)]
pub struct FileIO {
inner: Arc<dyn Storage>,
}
pub struct InputFile {
storage: Arc<dyn Storage>,
path: String,
}
pub struct OutputFile {
storage: Arc<dyn Storage>,
path: String,
} It seems like the impl InputFile {
...
pub async fn reader(&self) -> crate::Result<impl FileRead> {
...
}
}
impl OutputFile {
...
pub async fn writer(&self) -> crate::Result<Box<dyn FileWrite>> {
...
}
} Since both of these methods return an owned trait implementation the problems with them are similar. For To get around the above issues I experimented with embedding an implementation specific As I said earlier, I'm happy to post a draft PR with the full change set (there are multiple other items to work out that I'm ignoring here) if anyone would like to see it. If there are other ideas on how to make the |
I think that depends. This library provides several functionality for reading:
For integration with query engines like datafusion, datafusion only need to use planning file api, and perform scanning by itself. |
Hi, @BlakeOrth Thanks for trying this, and yes it's quite close to what's in my mind. So the question is that we can't keep the return type of
struct FileReadWrapper(Arc<dyn FileRead>);
impl FileRead for FileReaderWrapper {
...
} Solution 2 is expected maintain backward compatibility, but it's somehow ugly since it maintains another layer of unnecessary abstraction. There is a solution which combines the benefits of both:
|
I have debated filing this ticket for a while, but largely held off as I wasn't sure how well it would be received, especially as I am acutely aware that this crate currently makes use of OpenDAL and @Xuanwo is an active contributor to both repositories. However, I feel it is important to have these discussions, and part of my role as a maintainer of object_store is to engage with others in the community and hear about how its offering could be made more compelling.
That all being said, I think object_store provides some quite compelling functionality that might be of particular interest to this project:
The major area object_store is limited, somewhat intentionally, is in the number of first-party implementations; only supporting S3-compatible stores, Google Cloud Storage, Azure Blob Storage, in-memory and local filesystems. However, the object-safe design does allow for third-party implementations, for things like HDFS.
I look forward to hearing your thoughts, but also fully understand if this is not a discussion you would like to engage with at this time.
The text was updated successfully, but these errors were encountered: