Skip to content
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

[MM-46912] Add pagination #21026

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

[MM-46912] Add pagination #21026

wants to merge 5 commits into from

Conversation

vish9812
Copy link
Contributor

Summary

Accept a pagination limit from frontend.

Ticket Link

JIRA - https://mattermost.atlassian.net/browse/MM-46912

Related Pull Requests

Release Note

Used pagination params(page & per_page) available in api /channels/:channel_id/posts for the "since" route.

@vish9812 vish9812 added 2: Dev Review Requires review by a developer 3: QA Review Requires review by a QA tester. May occur at the same time as Dev Review labels Sep 14, 2022
@mattermod
Copy link
Contributor

E2E tests not automatically triggered, because PR has no approval yet. Please ask a developer to review and then try again to attach the QA label.

@mm-cloud-bot mm-cloud-bot added the release-note Denotes a PR that will be considered when it comes time to generate release notes. label Sep 14, 2022
@vish9812
Copy link
Contributor Author

@lieut-data Adding you in-case you want to have a look in "plugin" changes.

@zefhemel
Copy link
Contributor

cc @agnivade @agarciamontoro to make sure we're not introducing surprising performance issues here

@@ -682,8 +682,11 @@ func (api *PluginAPI) GetPost(postID string) (*model.Post, *model.AppError) {
return post, appErr
}

func (api *PluginAPI) GetPostsSince(channelID string, time int64) (*model.PostList, *model.AppError) {
list, appErr := api.app.GetPostsSince(model.GetPostsSinceOptions{ChannelId: channelID, Time: time})
func (api *PluginAPI) GetPostsSince(channelID string, time int64, page, perPage int) (*model.PostList, *model.AppError) {
Copy link
Member

Choose a reason for hiding this comment

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

At the RPC level, this should be fine, but semantically, I think it's still a breaking change if old plugins are expecting to get all the posts from a call to GetPostsSince.

Might we consider handling that by treating page == perPage == 0 specially?

At the same time, I'm also not 5/5 for this. It /feels/ unlikely that a plugin was depending on this behaviour, so we might be able to at least scan the known plugins for usage and make a judgment call here instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

old plugins are expecting to get all the posts

Even before it wasn't returning all the posts technically, since it always had a limit. It's just that, the limit was quite big.

scan the known plugins for usage

Any guidance on this, how or where can I verify it?

handling that by treating page == perPage == 0

You mean, don't limit/paginate in this case?

Copy link
Member

Choose a reason for hiding this comment

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

technically, since it always had a limit. It's just that, the limit was quite big.

@vish9812, for reference, what was that big limit?

Any guidance on this, how or where can I verify it?

@mickmister / @levb, I know we've done analysis of this kind in the past. (I sometimes do it locally just be grepping over my plugins src directories to see if anything matches.) Also interested in your feedback on maybe just ignoring it instead (allowing the "breaking change").

You mean, don't limit/paginate in this case?

Yeah, the idea would be to essentially revert to the big limit as previously. Not sure it's worth this, as noted above.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for the late response, grepping the local plugin repos is all I know of as far as analysis goes.

As to the breaking change, this is an interface change and will break compatibility with the old plugin's signatures, no? I am not sure if the extra parameter is a breaking change for the RPC or not. Was introducing a new plugin API, GetPostsSincePaginated considered while keeping the old one intact, perhaps as deprecated?

Copy link
Member

Choose a reason for hiding this comment

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

As to the breaking change, this is an interface change and will break compatibility with the old plugin's signatures, no? I am not sure if the extra parameter is a breaking change for the RPC or not.

@levb @lieut-data IIRC this is a breaking change when the argument types of the function change. The "safe" way to introduce an API change is to add fields to an existing struct being used, but that's not an option here.

Was introducing a new plugin API, GetPostsSincePaginated considered while keeping the old one intact, perhaps as deprecated?

+1 for introducing a new method. 3/5 An "ugly" method name is worth avoiding backwards compatibility issues

Copy link
Member

Choose a reason for hiding this comment

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

It looks like the change was reverted for plugins though. Makes sense as it seems this PR is specifically meant for the webapp

@agarciamontoro
Copy link
Member

Thanks for the ping, @zefhemel.

At a first glance, this does not seem harmful performance-wise, since we're only adding the OFFSET, and I assume the LIMIT (i.e. the perPage param) will be probably less than it was before, right?

@vish9812, I see that the webapp PR just adds the params. Where are we planning to use this new pagination?

@vish9812
Copy link
Contributor Author

I assume the LIMIT (i.e. the perPage param) will be probably less than it was before, right?

Yes, it's quite smaller than the previous one.

the webapp PR just adds the params

Yeah, but with default values. So there is no "new" use-case for it. It's just that it will be fetching the data in smaller batches now.

@agarciamontoro
Copy link
Member

So there is no "new" use-case for it. It's just that it will be fetching the data in smaller batches now.

But if pagination wasn't there, I guess we'll need to modify the clients now to use it, right? Or are all usages of this ok with always fetching 60 instead of 1000 posts in total?

@agnivade
Copy link
Member

Bear in mind that LIMIT/OFFSET doesn't really control the memory usage in any way of the DB. If you have 10,000 elements and want to load the last 100 elements, a LIMIT 100 OFFSET 9900 will still load 10,000 elements in the DB. That's why this approach is not recommended and it is suggested to use cursor based pagination.

See https://github.com/mattermost/mattermost-server/blob/1a5ecfa9572ef18f93cd13c99ad94fc8d548a820/store/sqlstore/post_store.go#L598 for what I am talking about.

@vish9812
Copy link
Contributor Author

vish9812 commented Sep 16, 2022

@agarciamontoro So far it seems to work with the smaller limit. I tried a few scenarios.

@agnivade As per the "since" api usage, it seems unlikely that it's gonna have a high Offset. There are "after" & "before" apis which are used more often to load the data. Its main usage seems to "sync" the posts, and during this "disconnected-client" time, 1000s of posts shouldn't be a common scenario. Also, at the moment it's similar to what "after" & "before" apis follow.

@agnivade
Copy link
Member

I think it's hard to say what is a common scenario here. If someone is gone for a week in a busy installation, it's not that far fetched for a busy channel to have several thousand posts.

In any case, this is more about API security than about API usage. Even if it's not common, someone can make such an API request and possibly crash the DB. Nothing can stop them.

All that being said, I understand that it's been always like this, so there's not a big urgency to change the model. And moreover, if we start using GraphQL for posts, we will automatically use a cursor based model.

I'll leave you to take a call.

@enahum
Copy link
Contributor

enahum commented Sep 17, 2022

I'm afraid I must block this PR until the changes with backwards compatibility are added to v1 and v2 of the mobile apps!!

@enahum enahum added the Do Not Merge/Awaiting PR Awaiting another pull request before merging (e.g. server changes) label Sep 17, 2022
@vish9812
Copy link
Contributor Author

@enahum I've added backwards compatibility. LMK, if it works or something else needs to be done.

@vish9812
Copy link
Contributor Author

this is more about API security than about API usage. Even if it's not common, someone can make such an API request and possibly crash the DB

@agnivade Yeah, makes sense. I'll discuss it with the team, as it would require more client-side updates, also it can become a little messy to use two types of pagination within a single function. So ideally, we should update the "after" and "before" routes as well to use the cursor based model.

@agnivade
Copy link
Member

Agree with your thinking @vish9812

@vish9812
Copy link
Contributor Author

@agnivade Created another jira-ticket to change it to the Cursor-based pagination.

@enahum Can you please confirm, if the backwards compatibility looks okay?

@enahum enahum self-requested a review October 12, 2022 13:39
enahum
enahum previously approved these changes Oct 18, 2022
Copy link
Contributor

@enahum enahum left a comment

Choose a reason for hiding this comment

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

couple of notes added.

Really important that the QA tester covers this on the v1 and v2 app to make sure nothing broke.

api4/post.go Outdated
@@ -179,13 +182,18 @@ func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.SetInvalidParam("since")
return
}

// In case of "since" route, return all items by default for now, to maintain
// backwards compatibility with mobile. But when the next ESR passes, just
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd say when the version of the server that ships this becomes the ESR this can be removed, not before.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should I just remove the ESR-part from the comment. That should automatically means, remove this "if" block whenever the min. mobile version starts supporting the new pagination, right?

@@ -1231,7 +1231,8 @@ func (s *SqlPostStore) getPostsSinceCollapsedThreads(options model.GetPostsSince
Where(sq.Gt{"Posts.UpdateAt": options.Time}).
Where(sq.Eq{"Posts.RootId": ""}).
OrderBy("Posts.CreateAt DESC").
Limit(1000).
Offset(uint64(options.Page * options.PerPage)).
Limit(uint64(options.PerPage)).
Copy link
Contributor

Choose a reason for hiding this comment

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

if perPage is 0, should we keep the limit of 1000 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@vish9812 vish9812 removed the Do Not Merge/Awaiting PR Awaiting another pull request before merging (e.g. server changes) label Nov 1, 2022
@vish9812
Copy link
Contributor Author

vish9812 commented Nov 1, 2022

@ashishbhate @koox00 It's up for review.

@vish9812
Copy link
Contributor Author

vish9812 commented Nov 1, 2022

/update-branch

@isacikgoz
Copy link
Member

@vish9812 is this PR active? If not can you please close it? Thanks!

@vish9812 vish9812 requested a review from kostaspt May 4, 2023 07:32
@@ -2033,9 +2033,9 @@ export default class Client4 {
);
};

getPostsSince = (channelId: string, since: number, fetchThreads = true, collapsedThreads = false, collapsedThreadsExtended = false) => {
getPostsSince = (channelId: string, since: number, page = 0, perPage = PER_PAGE_DEFAULT, fetchThreads = true, collapsedThreads = false, collapsedThreadsExtended = false) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not put the new params at the end of the method? It's not optimal context-wise, but it will improve backward incompatibility since the order of the params is important.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is to keep the signature similar to all other functions.

@agnivade agnivade removed their request for review June 9, 2023 03:32
@vish9812 vish9812 self-assigned this Jul 17, 2023
Copy link
Member

@agarciamontoro agarciamontoro left a comment

Choose a reason for hiding this comment

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

First of all, sorry for the delay on my review!

Changes look good, but I'm a bit concerned about the unforeseen effects, since we're effectively changing the default value of perPage from 1000 to 60, which is quite a large change. Now, I'm not against this change, but I feel like we need extensive testing before merging (probably including load-testing), since this change means that where we made one query before, we now do 17 (of course much smaller). Now, I don't know how frequent that case is: do we usually get thousands of posts from this API endpoint? Or is the code usually getting much less? I see that the case for third-party callers of this endpoint (plugin) is taken care of, and we're still defaulting to 1000, but the webapp is what concerns me the most.

So from a code point of view, this PR is perfect, but for it to be merged I'd ask for:

  • Extensive QA
  • Load-testing focused on the number and duration of the requests to this endpoint and its general effect in the overall health of the server.

So I guess the thing is: do we have enough resources for doing this? Or should we focus our efforts on something with more priority? I'll let you guys answer those.

Comment on lines +1264 to +1267
// Add original limit of 1000 to maintain backwards compatibility with mobile and plugins.
if options.PerPage == 0 {
options.PerPage = 1000
}
Copy link
Member

Choose a reason for hiding this comment

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

Should we set option.Page = 0 as well to make sure nothing weird happens in the default case?

@harshilsharma63 harshilsharma63 removed their request for review August 28, 2023 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2: Dev Review Requires review by a developer 3: QA Review Requires review by a QA tester. May occur at the same time as Dev Review release-note Denotes a PR that will be considered when it comes time to generate release notes.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet