-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Core: Freshness-aware table loading in REST catalog #14398
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
base: main
Are you sure you want to change the base?
Conversation
|
This change has 2 dependencies also included in the chain:
|
|
Let me give an initial insight into the different decisions I made during the implementation: 1) Client-side table cache Entire table objects are cached on the client side. The table objects hold user level configs and credentials, so in order to not share these accross users, the client-side table cache is arranged on a per-sessionID basis. For simplicity I chose to use a composite key: (SessionID, TableIdentifier) for the cache. Later if there is a need, the cache can be enhanced to have 2 separate levels for the sessionID and the TableIdentifier instead of a composite key. With this we can have different cache eviction policies for the sessions (max number if entries, TTL) and for the tables. Also we could separate sessions not to evict tables related to other sessions. 2) Table objects returned by loadTable Currently, each 3) Table commits As a direct result of the above point the table commits through 4) SnapshotMode = REFS When lazy snapshot loading is configured, loadTable will populate the cache with a table object that has partially loaded snapshot list. If the client calls snapshots() and loads the rest of the snapshots, this won't be reflected in the cache. As a future improvement the snapshotsSupplier provided to TableMetadata could be clever enough to refresh the cache too. I found thes too complicated to implement for the initial version. 5) Place of the ETag Initially I've put the ETag into 6) Cache eviction policies No weakKeys, weakValues (or similar) could be used with the current design: the returned tables are clones, so weakValues would nearly immediately evict the entry from the cache right after adding it. |
| context, | ||
| baseIdent, | ||
| snapshotMode, | ||
| headersForLoadTable(cachedTable), |
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.
This approach requires the table to have an entry in the cachedTable, so that we can retrieve its ETag and include it in the loadTable request. However, what if the ETag is cached externally, and the caller wants to supply the ETag directly when loading the table?
This use case is valid and common, the caller may store the ETag in somewhere for performance optimizations and expect to use it in the next loadTable request (may not use the same client).
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.
Thanks for taking a look, @XJDKC !
External ETags weren't in the scope of the original design. In fact the ETags for this functionality are deliberately kept 'under the hood' and not exposed to the clients/engines.
The use-case you describe wouldn't simply require the ETag to be passed in to loadTable() but also to have some mechanism to answer with a table object in case of a 304-NotModified. Would you simply expect null from loadTable() and have a cache on the caller side to answer such queries?
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.
Yeah, the caller might maintain a cache on the caller side. Many catalogs now support multiplexing, so the caller may have a cache to store the latest table metadata info.
For this use case, if the caller got an null pointer or something that indicates that the table is not modified, the caller needs to handle it by itself. e.g., don't need to proceed.
Does it make sense to allow the caller to pass in the ETag in the SessionContext? Maybe via a property in the property map?
(Noticed that, unfortunately, for RESTCatalog::loadTable, it doesn't allow the caller to pass in a SessionContext at table level, is it possible to add another interface?)
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.
Thank you for the explanation, @XJDKC !
I see your point. At the moment ETag handling, and answering from cache is deliberately kept internal to RESTSessionCatalog. One of the requirements when designing this was to not change existing interfaces or introduce new ones. Maybe we can re-visit this later on, but for simplicity and keeping the API clean I don't think we should provide a way to provide external ETags through the loadTable API
Additional observation I have is ETags are coming from the REST server, and are kept within the RESTSessionCatalog, but not persisted into the table object, nor into the table ops, so the users of loadTable API won't have a way to get the ETag of a table other than calculating it themselves. However, that goes against the purpose of an ETag, as it should be opaque to the clients.
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 fully understand your points, they make sense!
But just to add some context from real-world use cases: There are scenarios where the query engine tries to optimize loadTable performance by persisting the ETag somewhere. That way, when the engine restarts, it doesn't have to perform a full loadTable again and can instead reuse the previously fetched version.
It would be great if we could provide some flexibility or hooks for customization when implementing this!
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.
Could these users call the REST catalog API 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.
Thanks you for describing the use case @XJDKC !
When the engine restarts, it's not enough to persist the ETag somewhere because in order to answer a loadTable call you need a table object too.
Anyway, I still think that the scope of this PR should be as narrow as possible, and not exposing ETags is what we agreed on in the proposal doc. Once this work is done, further changes could be initiated.
380820b to
1fae134
Compare
core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
Outdated
Show resolved
Hide resolved
core/src/test/java/org/apache/iceberg/io/FileIOTrackerTestUtil.java
Outdated
Show resolved
Hide resolved
core/src/test/java/org/apache/iceberg/rest/TestableRESTSessionCatalog.java
Show resolved
Hide resolved
core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
Outdated
Show resolved
Hide resolved
core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
Outdated
Show resolved
Hide resolved
core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
Outdated
Show resolved
Hide resolved
1fae134 to
6c5c34d
Compare
|
Last change is a rebase with main |
6c5c34d to
e388d39
Compare
e388d39 to
4229598
Compare
|
Hi @danielcweeks @nastra @amogh-jahagirdar , |
|
Hi @danielcweeks @amogh-jahagirdar @nastra , |
|
LGTM, please rebase. |
4229598 to
fc0c6f6
Compare
|
Thanks for taking a look @pvary ! |
|
Conflicts with some of the recent changes around this area. Will rebase once this PR is merged. |
fc0c6f6 to
e1e173f
Compare
|
Rebased with main but apparently has another merge conflict. This time with the server-side planning changes |
|
|
||
| trackFileIO((RESTTableOperations) clonedTable.operations()); | ||
|
|
||
| tableCache.put( |
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.
Hey @singhpk234 ,
Let me invite you to review this PR, because I saw you had an overlapping change on this area with introducing the concept of RESTTable and server-side planning. I'm resolving merge conflicts here in the code and it made me wondering what should be the strategy for interworking the two functionalities: server-side planning and freshness-aware loading .
If I'm not mistaken, RESTTable is also a regular table like BaseTable, the difference is that some scan APIs are overridden to go to a different route when planning a query, right? Following the logic, it would still make sense to have ETags for such tables and cache them with freshness-aware loading functionality.
Would be great to hear your take on this! Thanks!
(I'll come back with the resolved code to work with both features soon)
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 managed to merge the 2 functionalities and resolve conflicts. I had to make seem adjustments. to your implementation though. E.g. RESTClient is one of the inputs for RESTTable, however, when doing my cloning of the table from cache, the client is not available only through RESTTableOps. Hence I had to give extra visibility to that member.
I also had to rewrite some of the code that returns RESTTable to handle the cloning from cache.
I'd appreciate if you could take a look @singhpk234
e1e173f to
65b826e
Compare
| * @param originalOps the ops to clone | ||
| * @return a new RESTTableOperations instance | ||
| */ | ||
| protected RESTTableOperations newTableOps(RESTTableOperations originalOps) { |
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.
@XJDKC @flyrain After I rebased my changes I observed that this freshness-aware loading functionality messes up your injectable table ops change. The reason in a nutshell is when I return a table from client-side cache, I have to clone it (in order to the clients not having a race condition on using the same table object), but when cloning the table the injected ops is lost.
As a solution, I introduced another variant of the newTableOps that is basically to clone on ops object into another one. With this design you'll have to override this new function too, whenever this PR is merged. I haven't found any other good way to solve this.
Would you mind taking a look?
c38ce1d to
6de4a79
Compare
|
There is a filing test in TestRESTScanPlanning, but that apparently fails across other PRs too. cc @singhpk234 |
|
Thanks @gaborkaszab i am not sure what is causing the flakyness as my CI green when we merged, i will take a deeper look and put a fix ASAP. |
9e7994c to
3120887
Compare
|
Update on the latest changes: As a side-effect, since we don't cache table (including ops and IO), the FileIOTracker tests are no longer needed within this PR. Removed them. cc @flyrain @XJDKC @singhpk234 since this intersects with the PRs you've merged recently. |
3120887 to
7a93a1f
Compare
|
Looks nice @gaborkaszab! Do we have tests for how the injectable table ops change interacts with the cache? |
7a93a1f to
4525eab
Compare
|
Thanks for taking a look, @pvary !
|
This is the client-side improvement for the freshness-aware table loading in REST catalog. The main design is the following: - REST server can send an ETag with the LoadTableResponse - The client can use this ETag to populate the IF_NONE_MATCH header with the next loadTable request - The server can send a 304-NOT_MODIFIED response without a body if the table has not been changed based on the ETag - The client when receives a 304, then returns the latest table object associated with the ETag from cache
4525eab to
8fc6ca8
Compare
|
This is great! Thanks @gaborkaszab and sorry that my merge ended up conflicting with your changes and require some rework on your end. |
This is the client-side improvement for the freshness-aware table
loading in REST catalog. The main design is the following:
- REST server can send an ETag with the LoadTableResponse
- The client can use this ETag to populate the IF_NONE_MATCH header
with the next loadTable request
- The server can send a 304-NOT_MODIFIED response without a body if
the table has not been changed based on the ETag
- The client when receives a 304, then returns the latest table
object associated with the ETag from cache