feat: provide inline_transaction model for IO optimizing#4774
feat: provide inline_transaction model for IO optimizing#4774wjones127 merged 3 commits intolance-format:mainfrom
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4774 +/- ##
==========================================
+ Coverage 81.75% 81.84% +0.08%
==========================================
Files 340 341 +1
Lines 140010 140204 +194
Branches 140010 140204 +194
==========================================
+ Hits 114469 114752 +283
+ Misses 21732 21626 -106
- Partials 3809 3826 +17
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| // if < 200KB, store the transaction content inline | ||
| bytes transaction_content = 19; | ||
| // if >= 200KB, store the transaction content at the specified offset | ||
| TransactionSection transaction_section = 20; |
There was a problem hiding this comment.
I was thinking we should just keep it as a separated file which seems to be more backwards compatible. Is there any other benefit in storing it at offset of the manifest?
There was a problem hiding this comment.
For backwards compatibility we should write both for a while. We can stop writing the external file after adding a feature flag 6+ months in the future. I've described the sequence of steps in #3487
wjones127
left a comment
There was a problem hiding this comment.
I think putting inline in the protobuf is necessary. We can put it at an offset and use existing optimizations to read it without an additional IOP.
| // The transaction that created this version. | ||
| oneof inline_transaction { | ||
| // if < 200KB, store the transaction content inline | ||
| bytes transaction_content = 19; | ||
| // if >= 200KB, store the transaction content at the specified offset | ||
| TransactionSection transaction_section = 20; | ||
| } |
There was a problem hiding this comment.
We should just unconditionally store the transaction at an offset, IMO. This makes this simpler, and it's still possible to read the transaction when it is small.
We currently store the index metadata at an offset, and here's how it works right now:
- When we read the manifest file, we always read the first
block_sizebytes (4kb for local fs, 64kb for object storage) - We decode as much as we can from that last chunk. If the entire manifest file is < 64kb, this often means we get the entire content (including the index metadata) in 1 IOP.
- If that last chunk wasn't sufficient, we read the remainder.
This basically gives the same effect as the optionally inline message: If it's small, we automatically read the index and transaction information in the first IO request. But if it's large, we do it in a separate request.
There was a problem hiding this comment.
Here's the place where we opportunistically read the index metadata from manifests:
| // if < 200KB, store the transaction content inline | ||
| bytes transaction_content = 19; | ||
| // if >= 200KB, store the transaction content at the specified offset | ||
| TransactionSection transaction_section = 20; |
There was a problem hiding this comment.
For backwards compatibility we should write both for a while. We can stop writing the external file after adding a feature flag 6+ months in the future. I've described the sequence of steps in #3487
| message TransactionSection { | ||
| // The summary of the transaction including operation type and some kvs in transaction_properties | ||
| // This is used to get some information about the transaction without loading the whole transaction content | ||
| map<string, string> summary = 1; |
There was a problem hiding this comment.
Hi, @wjones127 , What do you think of this part.
Basically it's redundent content for operation and transaction_properties. The case I want to solve now may encount listing historical manifest and transaction summaries. I'm not sure the opportunistically read could actually help this listing case if the manifest is large(actually the larger manifest we might need to accelerate it more).
On the other hand, some information may be useful if we could directly read from manifest like commit message. So I added this summary thing for directly reading from manifest without reading transaction part.
cc @jackye1995
There was a problem hiding this comment.
For the list transactions use case, I can’t help like feeling the best thing would be to cache compacted summary data in some secondary file. Like we could generate some Lance file next to the manifest that contains all transaction summaries before it. Then to get most of the history you just need to query that. Then read the transactions of the next few uncached versions. What do you think of that?
There was a problem hiding this comment.
How much performance do we want for this use case? I always thought reading information like transaction summary is more for non performance critical workloads, like displaying info to the user, triggerring some optimization jobs based on the action performed, etc. that can stand a few more IOPS.
There was a problem hiding this comment.
Like we could generate some Lance file next to the manifest that contains all transaction summaries before it
I had the same ideas. I thought lance might provide a lazy summary file includes tags and branches, summaries and properties. And could use the index framework to maintain this file. This was only an early idea. I could raise a discussion for it. Then we should ignore the summary here.
How much performance do we want for this use case? I always thought reading information like transaction summary is more for non performance critical workloads, like displaying info to the user, triggerring some optimization jobs based on the action performed, etc. that can stand a few more IOPS
For my case, one page displays at least 20 versions. each version should get a summary info includes manifest and transaction. If files seperated, the IO costs might have a magnification of 20x. Let's say read a transaction file costs 100ms,that means 2 seconds magnification at least.
There was a problem hiding this comment.
Yeah, I'm inclined to not have the summary there. Just keep it in the transaction. Pulling it out into the manifest means that listing is still O(num versions) IO requests. To make that fast, better to create some mechanism to query it.
non performance critical workloads, like displaying info to the user
I'd argue that displaying info to the user is somewhat performance critical, in that we'd like it to return fast enough to feel responsive.
There was a problem hiding this comment.
If files seperated, the IO costs might have a magnification of 20x. Let's say read a transaction file costs 100ms,that means 2 seconds magnification at least.
I'd argue that displaying info to the user is somewhat performance critical, in that we'd like it to return fast enough to feel responsive.
I was thinking we will just make the requests in parallel, so the latency won't really be 2 seconds, more like 200-300ms if anything got throttled and retried. But agree we need to make sure it is fast enough to feel responsive.
I thought lance might provide a lazy summary file includes tags and branches, summaries and properties. And could use the index framework to maintain this file.
that sounds like a nice idea, I originally created the concept of "system index" basically for such use cases.
9cd17b7 to
06e1a43
Compare
a93622b to
e61260c
Compare
This comment was marked as outdated.
This comment was marked as outdated.
|
Ready for review @wjones127 cc @jackye1995 |
wjones127
left a comment
There was a problem hiding this comment.
Nice work. Excited to have this in soon!
I'd like to see a test on IO. And I think disabling writing separate transaction files should enable a writer feature flag, which will alert older libraries of the incompatibility.
| // The file position of the transaction content. | ||
| // None if transaction is empty | ||
| optional uint64 transaction_section = 21; |
There was a problem hiding this comment.
Could you describe here how to calculate where the message ends?
| /// Control whether to also write an external transaction file. | ||
| /// Default is true for backward compatibility. Set to false to write inline only. | ||
| pub fn with_write_transaction_file(mut self, write_transaction_file: bool) -> Self { | ||
| self.write_transaction_file = write_transaction_file; | ||
| self | ||
| } |
There was a problem hiding this comment.
I don't think this should be a runtime setting. We should add a writer feature flag for only writing inline transaction files, make it off by default. Users can then enable it on table creation or with a later update. That will enforce older libraries don't try to write to this.
There was a problem hiding this comment.
Users can then enable it on table creation or with a later update
I guess that means directly deleting the code of write_transaction_file. And for the six months as you said, we kept the write_transaction_file and don't control it by any setting. And we update the feature flags by hardcode in some version in the future. Do I understand right?
The reason I ask is feature flags applying is after write_transaction_file, besides, we don't have a initialized feature flag to read in commit_new_dataset. I think if we want to dynamically control the behivour we still need some kind of setting. But I think this may be over thinking. Hardcode seems fine.
There was a problem hiding this comment.
And we update the feature flags by hardcode in some version in the future. Do I understand right?
Sorry, I think I wasn't clear. We should still have a setting.
The way you had it written before, users would choose on each commit whether to write the transaction file. I proposed instead they should decide that only once, when they create the dataset. Or they could decide later when they do a one-time migration of an existing dataset.
See how we do it for stable row id or v2 manifest paths, in WriteParams:
Thanks for the guide and review so far. I'm currently on a public holiday. I will adress your comments as soon as possible! |
|
Ready for another look @wjones127 |
wjones127
left a comment
There was a problem hiding this comment.
Generally looks good. Do you think we should have a setting? It would be nice to be able to enable it?
| /// Construct a format-layer Transaction from a protobuf Transaction. | ||
| pub fn from_pb(pb_tx: pb::Transaction) -> Self { | ||
| Self { inner: pb_tx } | ||
| } |
There was a problem hiding this comment.
This probably should just be a From<pb::Transaction> impl
| /// Control whether to also write an external transaction file. | ||
| /// Default is true for backward compatibility. Set to false to write inline only. | ||
| pub fn with_write_transaction_file(mut self, write_transaction_file: bool) -> Self { | ||
| self.write_transaction_file = write_transaction_file; | ||
| self | ||
| } |
There was a problem hiding this comment.
And we update the feature flags by hardcode in some version in the future. Do I understand right?
Sorry, I think I wasn't clear. We should still have a setting.
The way you had it written before, users would choose on each commit whether to write the transaction file. I proposed instead they should decide that only once, when they create the dataset. Or they could decide later when they do a one-time migration of an existing dataset.
See how we do it for stable row id or v2 manifest paths, in WriteParams:
Yeah, I did think we should have a setting.
I'm a little confused about this
I'm not sure if you are saying just modify the default value of |
|
@majin1102 Did you look at how stable row ids work? I think that's a good guide here. The user turns them on via This is passed down with And then it is saved in the feature flags: When we load the dataset, we call |
Thank you so much for the details! I was using open call in IDEA. Next time I will look more patiently and carefully(maybe with some simple tests) |
wjones127
left a comment
There was a problem hiding this comment.
This is looking great! Nice work
5de4f82 to
ba58719
Compare
|
@majin1102 did you still want to finish this? It seemed like it was almost done and then you moved it into draft. |
b797357 to
4ce1cca
Compare
Sorry for delaying so far. I didn't mean to leave this unfinished. I was encountering an assertion failure on windows after some code rebasing(assert IO times). I had some clue and tried to fix, but didn't work. I was intending to use some more time to look through this. But yeah there's some other parallel work and eventually led to the PR being consistently delayed. In the meanwhile I found load_new_transactions() was still using transaction files: I missed this part. I will complete this asap . Thanks again for the review and guide @wjones127 |
4ce1cca to
18625ce
Compare
After the latest rebase, all Windows tests pass. Looks like a recent commit fixed that windows issue. Feel free to merge if it looks good to you. Please let me know if you have any other comments! |
There was a problem hiding this comment.
💡 Codex Review
https://github.com/lancedb/lance/blob/18625cef89474db2baccb766e0e972d713ceaca7/rust/lance/src/dataset.rs#L2135-L2159
Transactions listing ignores inline transaction data
The new inline transaction support only updates Dataset::read_transaction; the transactions() stream still unconditionally loads _transactions/{file} and errors if no external file was written. When disable_transaction_file is enabled (or a transaction file is deleted as in the new tests), manifest.transaction_file remains an empty string, so this code attempts read_transaction_file and fails instead of using the inline transaction_section embedded in the manifest. As a result, datasets relying solely on inline transactions cannot enumerate history even though the manifest contains the data. The method should reuse Dataset::read_transaction or fallback to manifest.transaction_section before assuming a file exists.
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
3ad71b3 to
c419c4e
Compare
…t#4774) Hi, @wjones127, @jackye1995 , I'm working on issue lance-format#4308 lance-format#3487 In this PR I wanna proposing the inline_transaction model for IO optimizing. The motivation includes: 1. Get transaction summary without transaction file IO 2. Optimize commit by reducing transaction file IO
Hi, @wjones127, @jackye1995 , I'm working on issue #4308 #3487
In this PR I wanna proposing the inline_transaction model for IO optimizing. The motivation includes: