-
Notifications
You must be signed in to change notification settings - Fork 250
Implement pagination for list APIs #273
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
Conversation
polaris-core/src/main/java/org/apache/polaris/core/catalog/PageToken.java
Outdated
Show resolved
Hide resolved
polaris-core/src/main/java/org/apache/polaris/core/catalog/PageToken.java
Outdated
Show resolved
Hide resolved
polaris-core/src/main/java/org/apache/polaris/core/catalog/PageToken.java
Outdated
Show resolved
Hide resolved
polaris-core/src/main/java/org/apache/polaris/core/catalog/PageToken.java
Outdated
Show resolved
Hide resolved
polaris-core/src/main/java/org/apache/polaris/core/catalog/PageToken.java
Outdated
Show resolved
Hide resolved
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 can't resolve my own feedback. Can you close them? Thanks
polaris-core/src/main/java/org/apache/polaris/core/catalog/PageToken.java
Outdated
Show resolved
Hide resolved
polaris-core/src/main/java/org/apache/polaris/core/catalog/PageToken.java
Outdated
Show resolved
Hide resolved
polaris-core/src/main/java/org/apache/polaris/core/catalog/PageToken.java
Outdated
Show resolved
Hide resolved
850baf4
to
549c5ab
Compare
@eric-maynard let me know if you need any further help on this PR. |
This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days. |
What's the plan for this PR? Do we want to revive it? :) |
…isGenericTableCatalog-2
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 Eric !
I think the idea make sense to me, will need some more time to do deeper look.
} else { | ||
// In this case, we cannot push the filter down into the query. We must therefore remove | ||
// the page size limit from the PageToken and filter on the client side. | ||
// TODO Implement a generic predicate that can be pushed down into different metastores |
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.
+1
List<ModelEntity> rawData = | ||
this.store.lookupFullEntitiesActive( | ||
localSession.get(), catalogId, parentId, entityType, unlimitedPageSizeToken); |
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.
[doubt] Let say we want to 10 records and table had 10k records and we have filter currently since this is a client side filter. And client requested page size of 2 we will get 5 time 10 K records.
I was seeing similar issue had 2 options.
- Load all the entities in one shot
- keep triggering the queries in loop until we get the requested page results.
I am not sure which is lesser poison pill to swallow.
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.
Luckily, we do not use this filter very often at all... but yes, this situation is less than ideal.
If the metastore itself provided a lazily-fetched Iterator
here instead of a List
(presumably with its own pagination/cache built into the client), we should definitely prefer to use that. But I think the right place for that logic is in the metastore itself and not at this layer, so I kept with the simplest approach. I think we need a solution for predicate push down in place eventually anyway.
There is actually a third option as well, which is that we just return more results than the client asked for. This is what we currently do actually, since we don't respect the page token.
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.
Looking into this a little more, EclipseLink does support TypedQuery.getResultStream
, so maybe it's as simple as just refactoring lookupFullEntitiesActive
to return a Stream instead of a List. I'm not sure how well that actually performs though; we'd want to benchmark it.
edit: 😔
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.
Does listing requires now to make this transactionally consistent, consider for ex: we need to list records, and some record got added while we were listing ? whats the behaviour expected ?
for ex Snowflake id generator which NoSQL impl generates or the ID generator that i am writing just makes sure entity id is unique, it doesn't guarantee incremental, am I missing something ?
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 don't think this needs to be transactionally consistent. You can miss an entity, but you will never get the same entity twice.
Entity ID is guaranteed to always be unique.
I suppose there is a case where you transactionally add some table and drop some other table that was already returned in an existing page -- the list results in aggregate will be inconsistent with that transaction. But I think this is outside the realm of what we need to support for the time being. I would probably not want to fail either the list operation or the table update operation to ensure consistency when clients who care about transactional consistency have the option to list without a page token.
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 disagree that an inconsistency (duplicate and omitted) results is acceptable.
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 also have a strong concern that there's more data processed here than absolutely necessary.
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.
There are no duplicate or omitted results here if we define omitted as "not present in the results of a list when the entity was in fact present through the duration of the list". Results are only omitted when they are deleted in the middle of a list operation; that is, the entity is gone by the time we reach the page that should contain it.
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 @eric-maynard, this is a much needed thing for practical uses cases ! I think the approach looks good to me overall.
I have some open orthogonal question, may be I am missing some context :
- We change the BasePersistence / PolarisMetaStoreManager for this, are all stakeholders onboard for this ?
- Are Entity-Ids assumed to be monotonically increasing ? is this a requirement from Polaris ? As entity_id just needs to unique from what i am aware.
List<ModelEntity> rawData = | ||
this.store.lookupFullEntitiesActive( | ||
localSession.get(), catalogId, parentId, entityType, unlimitedPageSizeToken); |
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.
Does listing requires now to make this transactionally consistent, consider for ex: we need to list records, and some record got added while we were listing ? whats the behaviour expected ?
for ex Snowflake id generator which NoSQL impl generates or the ID generator that i am writing just makes sure entity id is unique, it doesn't guarantee incremental, am I missing something ?
.../java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkStore.java
Outdated
Show resolved
Hide resolved
PolarisConfiguration.<Boolean>builder() | ||
.key("LIST_PAGINATION_ENABLED") | ||
.catalogConfig("list-pagination.enabled") | ||
.description("If set to true, pagination for APIs like listTables is enabled") |
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 be explicit on which of the following APIs we will support pagination ?
@@ -183,4 +183,12 @@ protected FeatureConfiguration( | |||
"How many times to retry refreshing metadata when the previous error was retryable") | |||
.defaultValue(2) | |||
.buildFeatureConfiguration(); | |||
|
|||
public static final PolarisConfiguration<Boolean> LIST_PAGINATION_ENABLED = |
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.
can this be configured at client level ?
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 should be there at both levels. The client can choose to call with or without a page token. But we need a feature flag on the server side.
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.
But we need a feature flag on the server side.
What's the reason for that? Whether an Iceberg client supports pagination is clear from the request.
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.
As the feature is somewhat large in scope, I think it makes sense to feature flag it off. There are regression considerations; imagine you deploy Polaris with a metastore implementation which you later find will perform poorly when page tokens are used. It could make sense to disable page token support for a time being to improve performance, even if clients are providing page tokens.
Do you think this should rather be a behavior-change flag?
* @return list of tasks to be completed | ||
*/ | ||
@Nonnull | ||
EntitiesResult loadTasks(@Nonnull PolarisCallContext callCtx, String executorId, int limit); | ||
EntitiesResult loadTasks( | ||
@Nonnull PolarisCallContext callCtx, String executorId, PageToken pageToken); |
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.
[orthogonal] Are we ok in changing this interface ?
cc @dennishuo
@@ -29,6 +29,7 @@ dependencies { | |||
implementation(project(":polaris-api-management-service")) | |||
implementation(project(":polaris-api-iceberg-service")) | |||
implementation(project(":polaris-api-catalog-service")) | |||
implementation(project(":polaris-jpa-model")) |
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.
[doubt] why do we need this here ?
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 because ModelEntity
is one of the types that EntityIdPageToken.updated
is intended to support. It allows the metastore layer to update the existing page token with a List<ModelEntity>
. If we can remove this dependency I'm okay with it. To be honest, this looked very different before the persistence refactor and this could be my misunderstanding when resolving conflicts.
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 polaris-jpa-model
is only used by the EclipseLink, what about the other persistence implementations, e.g., jdbc one? I don't think the service module can depend on a specific persistence impl.
...ervice/src/test/java/org/apache/polaris/service/quarkus/catalog/GenericTableCatalogTest.java
Outdated
Show resolved
Hide resolved
public class ReadEverythingPageToken extends PageToken { | ||
|
||
private ReadEverythingPageToken() { | ||
this.pageSize = Integer.MAX_VALUE; |
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.
[doubt] Do we have checks further down in the persistence to not push down LIMIT when we need INT_MAX ?
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.
Currently we do actually push this limit down and apply it; it's up to each metastore how to implement this but both implementations seem to handle this okay.
|
||
public int pageSize; | ||
|
||
public static final PageToken DONE = null; |
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.
Suggestion: NONE
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.
Semantically, it's the content of the page-token in a response when there are no more pages; NONE sounds to me like it's intended to be used when there is no page token in the response at all. Of course, they are one in the same...
edit: One other thought -- when we have an Optional<PageToken>
, it seems like there could be ambiguity between Optional.of(PageToken.NONE)
and Optional.empty()
. Maybe I only feel that way because Scala uses None
for an empty option though
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.
Minor: I'd suggest END
, which seems a bit more natural than DONE
*/ | ||
public abstract class PageToken { | ||
|
||
public int pageSize; |
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.
Why is page size part of the page token? Why would constructing the next page have from the token have to depend on this page size?
Also, page size may be restricted by the total serialized size, not just by the number of items.
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 mostly just convenient to pass them around together. It minimizes API changes as you always need one if you need the other. It's also ergonomic to be able to do like pageTokenBuilder.fromLimit
and then treat that just as you would a page token that you got from a client.
There's also nothing inherently wrong about a page token that contains metadata about the requested page size. That said, PageToken
could be a bit misleading if you're expecting it to 1:1 correspond to the page-token
in the Iceberg spec. From that perspective it's more like a PageRequestDescriptor
or something; I'm very open to changing the name and just reached for PageToken
as the most obvious choice.
Also, page size may be restricted by the total serialized size, not just by the number of items.
Yeah the Iceberg spec just states:
an upper bound of the number of results that a client will receive
So I think int pageSize
captures this, but in theory you could have different PageToken implementations that treat this differently. For example you could have EntityIdPageTokenWithBytesLimit
where pageSize
is treated as a number of bytes rather than a number of records. The metastore implementations that support this PageSize
would have to be amended accordingly..
So far, everything is just on entity count so I didn't canonicalize this into the type structure but I think refactoring it would make sense if we ever want such a PageToken
(name pending)
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 noticed this elsewhere as well. Page token is a an opaque server generated value - page size is a hint from a client. Two very different things.
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.
"Opaque" means it can in fact contain the page size :)
But we are conflating two things. The real issue here is that we have a class called PageToken
which does not represent the page-token
parameter that the API receives/returns. It covers both that and page-size
. If this is contentious, I suggest the alternative name PageRequestDescriptor
. WDYT?
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'd prefer a name like PageRequest
.
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'm a little apprehensive about PageRequest
because it sounds similar to java.net.http.HttpRequest
and I think it might imply some CDI-related stuff (such as being RequestScoped). But maybe we can hash this out on the new interface-only PR
/** | ||
* A {@link PageToken} implementation that tracks the greatest ID from either {@link | ||
* PolarisBaseEntity} or {@link ModelEntity} objects supplied in updates. Entities are meant to be | ||
* filtered during listing such that only entities with and ID greater than the ID of the token are |
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.
Do we assume that entities are loaded in the order of their IDs?
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.
Metastores that use this PageToken
implementation must enforce 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.
If we want to add some new method to the metastore to canonicalize this check I think we can. The current check is similar to some other checks we have throughout the codebase that enforce a certain entity type is passed in.
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.
Oops, I missed that this page token was specific to JPA 🤦
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, it's a little brittle though. Related to your comment above about the varying semantics of pageSize
it could make sense to add a method to the metastore interface like supportedPageTokenTypes
or something.
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.
Noting: the order of values returned by database sequences is not necessarily strictly increasing. Aka: it is wrong to assume that each retrieved values from a database sequence is higher than any value retrieved before.
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 not an assumption; it's something the metastore has to enforce in order for this page token to be a valid implementation to use.
"SELECT m from ModelEntity m " | ||
+ "where m.catalogId=:catalogId and m.parentId=:parentId and m.typeCode=:typeCode and m.id > :tokenId"; | ||
|
||
if (pageToken instanceof EntityIdPageToken) { |
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 if it is not an EntityIdPageToken
?
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.
Then we should have failed above
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.
Not quite... The check is an OR
🤔
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 guess what I meant to say is that having logic conditional on the runtime type of the PageToken
looks risky to me... and hard to evolve/maintain.
I'm totally fine with each meta store impl. having its own PageToken
implementation, but I do not think token polymorphism makes sense at this level.
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.
Ah, I think I understand your comment better now. You are pointing out that depending on the page token type, the results may or may not get sorted by entity ID? That's true.
Not sorting is more of an optimization for the case where we have a ReadEverythingPageToken
(i.e. no page token). We could remove this optimization and always sort, but I added it to improve performance as in the majority of cases we have no page token.
In fact, I am trying not to couple page token implementations with metastore implementations here. In theory, the treemap metastore could also work with EntityIdPageToken.
I agree that the runtime check is a little awkward -- although we have such runtime checks all over the place currently. Is there a way you think we can achieve this same behavior without one?
I'm really hesitant to just always sort here. On the other hand, I think adding methods to PageToken
that are really only relevant to one implementation (e.g. requiresSortingByEntityId
) does not make a ton of sense from the interface perspective. Further, I think flipping the dependency between the metastore and page token types (e.g. PageToken::rewriteEclipseLinkQueryAsNeeded
) is not a good pattern either.
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.
Thx for that extra insight. I need some time for a deeper review 😅 ⏳
I think these interfaces probably must change in order to support efficient pagination, but I leave it to the reviewers here to determine if the feature is worth the changes.
They are not assumed to be monotonically increasing by this design, and it's not a requirement. In the EclipseLink store here, we sort by entity ID and return them in that order because it's durable against renames unlike sorting by identifier. But we do not care if some new table is created at a lower entity ID than the current "tail" of the paginated listing operation. This does mean that the consistency semantics are somewhat nondeterministic from a user POV. That is, if you create a table during a paginated list operation it's not clear whether that table will show up in the list or not. I think this is actually okay though, as you are literally racing one operation against the other. It's okay (if not ideal) to miss tables that are created after the listing operation is initiated. It's not okay to double-count a renamed table or to miss a table that is actually present during the entire list. The current PR satisfies these principles. We could make this more deterministic/consistent by adding an explicit predicate to the list operation to filter out tables that were created after the listing operation started. Happy to change this and take it on as another principle to adhere to. My reasoning has been that if we happen to discover a table created during the list we might as well include it. |
* are meant to be filtered during listing such that when a token with offset N is supplied, the | ||
* first N records are omitted from the results. | ||
*/ | ||
public class OffsetPageToken extends PageToken { |
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 mechanism can lead to duplicate results and/or omitted results.
The order of the entities returned is undefined - it can vary on every list() invocation.
Even if the order is (somewhat?) deterministic, a no-longer existing entity in a previous page will lead to a missing result - and vice versa for an entity added "in between" a previous page will lead to duplicate results.
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.
Whether or not this is true depends on the metastore implementation. Just as with the other PageToken implementation, the responsibility falls on the metastore to use a PageToken implementation that enforces the correct semantics. See this comment for another example of this.
i.e. EntityIdPageToken
is only usable if by a metastore if it can enforce that the entities in one list operation can be made to always be > the entity ID described by a page token, and that this will result in no duplicates or omitted results. The same is true for OffsetPageToken
, but instead of entity IDs it's offsets.
Notably, OffsetPageToken
is not really usable by the EclipseLink metastore right now but it's rather used for testing and just as an example.
/** | ||
* A {@link PageToken} implementation that tracks the greatest ID from either {@link | ||
* PolarisBaseEntity} or {@link ModelEntity} objects supplied in updates. Entities are meant to be | ||
* filtered during listing such that only entities with and ID greater than the ID of the token are |
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.
Noting: the order of values returned by database sequences is not necessarily strictly increasing. Aka: it is wrong to assume that each retrieved values from a database sequence is higher than any value retrieved before.
List<ModelEntity> rawData = | ||
this.store.lookupFullEntitiesActive( | ||
localSession.get(), catalogId, parentId, entityType, unlimitedPageSizeToken); |
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 disagree that an inconsistency (duplicate and omitted) results is acceptable.
* A {@link PageToken} implementation for readers who want to read everything. The behavior when | ||
* using this token should be the same as when reading without a token. | ||
*/ | ||
public class ReadEverythingPageToken extends PageToken { |
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 mixes two different things:
A "page token" is meant as an (opaque) piece of data to indicate where the next page result should start.
"Read everything" is a filter criteria, not a page token.
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.
See this comment for details on why these things are packaged together and named the way they are. The proposed alternative name there is PageRequestDescriptor
.
...is-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/ListEntitiesResult.java
Outdated
Show resolved
Hide resolved
@@ -183,4 +183,12 @@ protected FeatureConfiguration( | |||
"How many times to retry refreshing metadata when the previous error was retryable") | |||
.defaultValue(2) | |||
.buildFeatureConfiguration(); | |||
|
|||
public static final PolarisConfiguration<Boolean> LIST_PAGINATION_ENABLED = |
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.
But we need a feature flag on the server side.
What's the reason for that? Whether an Iceberg client supports pagination is clear from the request.
*/ | ||
public abstract class PageToken { | ||
|
||
public int pageSize; |
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 noticed this elsewhere as well. Page token is a an opaque server generated value - page size is a hint from a client. Two very different things.
@Nonnull Function<PolarisBaseEntity, T> transformer, | ||
@Nonnull PageToken pageToken) { | ||
List<T> data; | ||
if (entityFilter.equals(Predicates.alwaysTrue())) { |
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.
Does Predicate.alwaysTrue
return something that can be compared and will that hold true for future?
Predicate
s are rather functions/lambdas - those are not meant to be compared with anything.
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.
That's right. This is only meant to capture the predicate that's provided in the method overload that doesn't have a predicate, e.g. here. It is not meant to capture any predicate that's semantically "always true".
List<ModelEntity> rawData = | ||
this.store.lookupFullEntitiesActive( | ||
localSession.get(), catalogId, parentId, entityType, unlimitedPageSizeToken); |
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 also have a strong concern that there's more data processed here than absolutely necessary.
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public class PageTokenTest { |
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 wish there'd be much better test coverage about all the code and use cases have are changed in this PR.
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 this is fair. My understanding was that we may get quite of bit of testing for paginated listing "for free" from the Iceberg tests. Is that true, after all?
.description( | ||
"If set to true, pagination for APIs like listTables is enabled. The APIs that" | ||
+ " currently support pagination are listTables, listViews, and listNamespaces.") | ||
.defaultValue(false) |
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.
+1 on false by default.
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 @eric-maynard for working on it.
A cursor-based approach like EntityIdPageToken is admittedly more involved, but it gives us better consistency, stability, and performance at scale, so I’d prefer we go with that. I’m also not sure we need to support multiple approaches at this point.
The current PR touches quite a few areas, which makes it a bit hard to review in one go. Would it make sense to split it?
- First PR: focus on the pagination interfaces and add support in the in-memory metastore, so we can align on the contract.
- Follow-up PRs: add JDBC/EclipseLink support once the interface is settled.
WDYT?
* A wrapper for a {@link List} of data and a {@link PageToken} that can be used to continue the | ||
* listing operation that generated that data. | ||
*/ | ||
public class PolarisPage<T> { |
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.
OK with either, but leaning toward Page
, which is not only shorter, but also consistent with the naming within the package pagination
.
public final PageToken pageToken; | ||
public final List<T> data; |
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 public scope is generally fine here, but with the concerns that we will be in a bad situation in case of adding any processing on either of the field. Still recommend to make them private, and using getter.
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.
Nit: data
-> items
or entities
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.
a bad situation in case of adding any processing on either of the field.
Can you say more about this? The fields are final, so using a getter is functionally the same as just using the member.
This is essentially a record
but within the module that uses Java 11
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 public scope makes sense if they are final.
polaris-core/src/main/java/org/apache/polaris/core/persistence/pagination/PolarisPage.java
Show resolved
Hide resolved
|
||
public int pageSize; | ||
|
||
public static final PageToken DONE = null; |
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.
Minor: I'd suggest END
, which seems a bit more natural than DONE
public int pageSize; | ||
|
||
public static final PageToken DONE = null; | ||
public static final int DEFAULT_PAGE_SIZE = 1000; |
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.
Are we going to make it configurable at the server side? Not a blocker.
@@ -29,6 +29,7 @@ dependencies { | |||
implementation(project(":polaris-api-management-service")) | |||
implementation(project(":polaris-api-iceberg-service")) | |||
implementation(project(":polaris-api-catalog-service")) | |||
implementation(project(":polaris-jpa-model")) |
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 polaris-jpa-model
is only used by the EclipseLink, what about the other persistence implementations, e.g., jdbc one? I don't think the service module can depend on a specific persistence impl.
* filtered during listing such that only entities with and ID greater than the ID of the token are | ||
* returned. | ||
*/ | ||
public class EntityIdPageToken extends PageToken { |
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 JPA-model module should only be used by EclipseLink. We need to either found a common place to put this class, or duplicate it in different persistence implmentations.
* are meant to be filtered during listing such that when a token with offset N is supplied, the | ||
* first N records are omitted from the results. | ||
*/ | ||
public class OffsetPageToken extends PageToken { |
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.
Do we need an offset-based approach? I think we could use just one approach(cursor-based one, like EntityIdPageToken
) everywhere in Polaris.
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 in-memory metastore doesn't necessarily sort on Entity ID, and in the future we could paginate things that don't have an entity ID.
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 in-memory metastore doesn't necessarily sort on Entity ID
I think it's fine to sort on id, the perf concern would be minor since the in-memory metastore won't be used at scale.
in the future we could paginate things that don't have an entity ID.
We could still use a cursor-based approach when entity id is missing. The offset approach isn't necessary.
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 offset is the cursor. The cursor is based on row index.
*/ | ||
public abstract class PageToken { | ||
|
||
public int pageSize; |
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'd prefer a name like PageRequest
.
Description
The Apache Iceberg REST catalog spec describes how APIs like
listNamespaces
can implement pagination, but Polaris currently does not implement pagination.This PR implements pagination for
listNamespaces
,listViews
, andlistTables
. It also introduces a framework that could be used to implement pagination for other APIs likelistCatalogs
.Fixes #147
Type of change
Please delete options that are not relevant.
How Has This Been Tested?
Added tests in
IcebergCatalogTest
.Manual testing confirms that the
page-size
limit applies, and that page tokens can be used to continue a listing operation: