Skip to content

feat(158): suggest has_changed flag and clarify expectations about stale paginated list data #278

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

gregrperkins-roblox
Copy link

@gregrperkins-roblox gregrperkins-roblox commented Feb 14, 2025

The pagination AEP-158 doesn't give clear recommendations about what to do if the underlying data has changed during the course of a paged request. Normally the recommended cursor-based pagination strategy would continue serving stale data.

This PR suggests some changes to clarify this expectation, and provide a workaround when data freshness is critical.

Specifically, it adds a has_changed boolean to the list response, which clients that need up-to-date data can use to show an indicator and add a refresh listing action to their user interface.

🍱 Types of changes

  • Enhancement
  • New proposal
  • Migrated from google.aip.dev
  • Chore / Quick Fix

General

💝 Thank you!

gregrperkins-roblox and others added 7 commits February 14, 2025 11:46
The pagination AIP-158 doesn't give clear recommendations about what to do if the underlying data has changed during the course of a paged request. Normally the recommended cursor-based pagination strategy would continue serving stale data.

This PR suggests some language to clarify this expectation, and provide a workaround when data freshness is critical.

Specifically, it adds an optional `has_changed` boolean to the response, which clients that need up-to-date data can use to show an indicator and add a refresh listing action to their user interface.

(roblox internal DSA-4035)
@gregrperkins-roblox gregrperkins-roblox requested a review from a team as a code owner February 14, 2025 20:34
pagination to an existing method.
Methods returning collections of data **must** provide pagination _at the
outset_, as it is a [backwards-incompatible change](#backwards-compatibility)
to add pagination to an existing method.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prettier had some things to say about the main branch version of these files. I assume including its output verbatim is the preferred behavior, since https://aep.dev/contributing/#formatting is a broken link (no "formatting" section on that page).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rambleraptor re broken link on the site

And yeah I'm surprised this got checked in, given that we have a prettierrc specifying 79-char columns and GH workflow that looks like it runs a prettier check.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which version of prettier are you using? it didn't format these files for me, and it's good to dig into why.

re the broken link I think we just need to fix the markdown, I'll see if I can file a PR.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used VSCode's integrated prettier by flipping the file type to markdown.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the difference has to do with the prettier version. Here's a PR to upgrade it: #280.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should all be fixed at this point.

@@ -112,6 +113,9 @@ result being a resource.
- The response message **must** include a field corresponding to the resources
being returned, named for the English plural term for the resource, and
**should not** include any other repeated fields.
- The response message **may** include a `bool has_changed` field, to indicate
that the underlying data has changed since the `next_page_token` was
generated.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is ignored, i.e. the client just keeps paginating rather than starting over, is it set on every subsequent request as well? I can see why that would not really make sense; it would imply that the API continues to respond with next page tokens that reflect the previous state of the world, which is likely infeasible most of the time.

But I'm wondering about the behavior of client libraries that wrap pagination with an iterator (or even just a sync method that concatenates a number of paged responses). How would those handle this field?

Copy link
Author

@gregrperkins-roblox gregrperkins-roblox Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're right that it is worded wrong. My intention was that has_changed would be marked on all requests after there was a change. Services whose databases use insensitive (snapshot-based) cursors, like Postgres, could currently keep serving stale results from their db snapshot -- assuming they can keep track of the db cursor based on the next_page_tokens.

As worded it would be extremely uncommon to be able to support this, of course, since you'd have to detect changes since the prior request rather than the initial request. So that wording would need to be changed -- as a client you really want to know whether the data has changed since the initial request.

However, even beyond the wording, I also didn't really consider how such services would detect that the results had changed since their initial request, and... well, that doesn't seem like it's something that db systems normally support, nor does it seem reasonable to build in the application layer. The best implementation direction that is coming to me right now would be:

  • keep an updated_time db field
  • store a timestamp with the db cursor in the application layer that can be looked up based on the next_page_token
  • rerun the underlying list query every request but with a constraint to check if any of the fields had been updated since the db cursor was created
  • ...but this doesn't handle deletions... that might require something like postgres logical replication or some oracle aq beast of a system

If detecting whether there were changes since the snapshot isn't really feasible, then both of the recommendations here aren't either. Anyone know of any db systems that have some cute trick up their sleeve to detect whether data would have changed since the snapshot?

Assuming this isn't feasible, this PR would just collapse down to clarifying that APIs are usually built in a way that might return stale data when using the recommended paging cursors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thoughtful analysis! Agreed that it's generally uncommon for databases to be able to tell you that the data was modified while it's reading in rows for the request.

I think the clarification makes sense - we do have a "rationale" section that is available in aeps to explain why a particular choice was made: https://aep.dev/8/#document-structure.

I'm thinking about the ramifications to the clients a little more - and I believe that inherently there is some understanding that any request point-in-time can quickly become stale. This is why, for example, HTTP has the concept of an ETAG for a resource to verify whether a resource has changed: https://aep.dev/154/. Declarative providers like Terraform perform a get to validate state before and after. UIs often provide a refresh button.

So although I think it is an implicit expectation of clients to possibly read stale data, I think it's definitely worth a clarification, especially for list / pagination.

pagination to an existing method.
Methods returning collections of data **must** provide pagination _at the
outset_, as it is a [backwards-incompatible change](#backwards-compatibility)
to add pagination to an existing method.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rambleraptor re broken link on the site

And yeah I'm surprised this got checked in, given that we have a prettierrc specifying 79-char columns and GH workflow that looks like it runs a prettier check.

- If the end of the collection has been reached, the `next_page_token` field
**must** be empty. This is the _only_ way to communicate
"end-of-collection" to users.
- If the end of the collection has not been reached (or if the API can not
determine in time), the API **must** provide a `next_page_token`.
- Response messages for collections **may** provide an integer (`int32` for
protobuf) `total_size` field, providing the user with the total number of items
in the list.
protobuf) `totalSize` field, providing the user with the total number of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
protobuf) `totalSize` field, providing the user with the total number of
protobuf) `total_size` field, providing the user with the total number of

Protobuf fields are in snake_case.

been deleted or modified since the initial request, and paging through the list
**may** not include all current items.

If the service is capable of detecting underlying changes in the listing since
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If the service is capable of detecting underlying changes in the listing since
If the service is capable of detecting underlying changes in the list of results since

Not sure about the term "the listing" in this context...sounds to me like a marketplace term.

Comment on lines +168 to +169
using an in-memory storage mechanism without snapshots, it **may** return stale
data or phantom reads with no indication.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it document that this may happen?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, should would maintain backward compatibility, so that sounds like a good idea.

@@ -112,6 +113,9 @@ result being a resource.
- The response message **must** include a field corresponding to the resources
being returned, named for the English plural term for the resource, and
**should not** include any other repeated fields.
- The response message **may** include a `bool has_changed` field, to indicate
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_changed reads as a little vague to me. Maybe results_changed?

I kind of wanted to suggest something like page_token_expired, but I think that's overloading the concept. :/

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Later this has

the API may expire the page token.

results_changed sounds better, yeah

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants