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
base: master
Are you sure you want to change the base?
[MM-46912] Add pagination #21026
Conversation
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. |
@lieut-data Adding you in-case you want to have a look in "plugin" changes. |
cc @agnivade @agarciamontoro to make sure we're not introducing surprising performance issues here |
app/plugin_api.go
Outdated
@@ -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) { |
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.
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.
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.
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?
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.
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.
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.
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?
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 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
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 looks like the change was reverted for plugins though. Makes sense as it seems this PR is specifically meant for the webapp
Thanks for the ping, @zefhemel. At a first glance, this does not seem harmful performance-wise, since we're only adding the @vish9812, I see that the webapp PR just adds the params. Where are we planning to use this new pagination? |
Yes, it's quite smaller than the previous one.
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. |
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? |
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 See https://github.com/mattermost/mattermost-server/blob/1a5ecfa9572ef18f93cd13c99ad94fc8d548a820/store/sqlstore/post_store.go#L598 for what I am talking about. |
@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. |
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. |
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 I've added backwards compatibility. LMK, if it works or something else needs to be done. |
@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. |
Agree with your thinking @vish9812 |
@agnivade Created another jira-ticket to change it to the Cursor-based pagination. @enahum Can you please confirm, if the backwards compatibility looks okay? |
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.
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 |
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 say when the version of the server that ships this becomes the ESR this can be removed, not 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.
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?
store/sqlstore/post_store.go
Outdated
@@ -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)). |
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 perPage is 0, should we keep the limit of 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.
@ashishbhate @koox00 It's up for review. |
/update-branch |
@vish9812 is this PR active? If not can you please close it? Thanks! |
13e93db
to
a40f19f
Compare
@@ -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) => { |
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 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.
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 is to keep the signature similar to all other functions.
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.
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.
// Add original limit of 1000 to maintain backwards compatibility with mobile and plugins. | ||
if options.PerPage == 0 { | ||
options.PerPage = 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.
Should we set option.Page = 0
as well to make sure nothing weird happens in the default case?
Summary
Accept a pagination limit from frontend.
Ticket Link
JIRA - https://mattermost.atlassian.net/browse/MM-46912
Related Pull Requests
Release Note