Add new API to list paginated qualified conv ids#1686
Conversation
cee2135 to
5cc51aa
Compare
It doesn't fail the way I want it tho!
227743b to
5888386
Compare
pcapriotti
left a comment
There was a problem hiding this comment.
This looks good, but I would suggest going the extra mile and try improving the way pagination works, since we are changing the API anyway.
Specifically, I think it would be nice if we could, at the same time:
- get rid of the
max + 1trick, since getting an extra empty page at the end is not a big problem, and only happens if the page size happens to divide the total number of conversations; - actually use Cassandra's pagination support, instead of doing it by hand; then we wouldn't need to make two queries to paginate remote conversations;
- get rid of the awkward
ResultSettype.
To that end, instead of using paginate, we can directly extract the Metadata field from the result of a query, and look at the pagingState value in there (see the code of paginate itself). This value can be returned to the client, and later passed to subsequent queries. Of course, we would need to augment it with a bit that tells us whether we need to resume a local conversation query or a remote one.
I haven't looked at the tests too carefully, but it looks like they are pretty thorough. Minor comments follow.
services/galley/src/Galley/Data.hs
Outdated
| -- Otherwise, reads 'max' records starting from the 'start' parameter. Doing | ||
| -- this is unfortunately not trivial, so this function first gets all the | ||
| -- conversations which match the domain and then if there is still space, gets | ||
| -- the conversations which have domain > domain of start. |
There was a problem hiding this comment.
I imagine this is necessary because there is no way in cassandra to say (domain, id) > (start_domain, start_id)?
There was a problem hiding this comment.
Not exactly this, but there is a token function. But its behaviour depends on some Cassandra settings, I am going to try to use it and if it works, maybe explore how Cassandra will behave under different settings.
There was a problem hiding this comment.
From my reading so far token can only be used on "partitiion keys". Partition keys are keys which are listed in the first column of the primary key. Right now our primary key is (user, remote_conv_domain, remote_conv_id), so our primary key is just user. If we want this to work, I guess we'll have to change this to something more complex like ((user, remote_conv_domain, remote_conv_id), user). And then set the cluster ordering by user. This may allow us to write queries like WHERE token(user, remote_conv_domain, remote_conv_id) > (?, ?, ?) AND user = ?. Not sure what other problems this might cause, I am already thinking this might cause data for one user to be scattered across different nodes, which might not be ideal. I am gonna read more.
There was a problem hiding this comment.
Actually, the partition key doesn't need to be unique, so we can even get rid of user from it, making our primary key to be ((remote_conv_domain, remote_conv_id), user)
There was a problem hiding this comment.
But why not use pagingState instead and let the driver handle it? Or am I missing something?
There was a problem hiding this comment.
But why not use pagingState instead and let the driver handle it? Or am I missing something?
I will try this next and see if it works.
There was a problem hiding this comment.
This actually works 🎉
services/galley/src/Galley/Data.hs
Outdated
| nextPage <- toResultSet remainingMax <$> paginate Cql.selectUserRemoteConvsFromDomain (paramsP Quorum (usr, d) (remainingMax + 1)) | ||
| pure $ nextPage {resultSetResult = resultSetResult domainPage <> resultSetResult nextPage} |
There was a problem hiding this comment.
This idiom of using max + 1 then discarding the last element is used in multiple places, so it would be good to extract it in a convenience function.
There was a problem hiding this comment.
This is no longer required, so maybe we can just retire this thing soon.
mdimjasevic
left a comment
There was a problem hiding this comment.
I can't say I understand the code well. I guess the main sections are in the Data and Query modules, but I don't understand them properly. If no one else approves the PR, let me know and let's go through the code together.
Smaller observations are inlined in the code.
Co-authored-by: Marko Dimjašević <marko.dimjasevic@wire.com>
Co-authored-by: Marko Dimjašević <marko.dimjasevic@wire.com>
Co-authored-by: Marko Dimjašević <marko.dimjasevic@wire.com>
mdimjasevic
left a comment
There was a problem hiding this comment.
This looks good! I left minor comments and some questions inlined.
| <*> pageHasMore .= field "has_more" schema | ||
| <*> pagePagingState .= field "paging_state" schema | ||
|
|
||
| -- | TODO: Would be nice to not expose these details to clients |
There was a problem hiding this comment.
Are you planning to take care of the TODO as part of this PR? If not, it should be a future work note.
There was a problem hiding this comment.
Ah yes, I forgot to ask y'all if you had any better ideas. Do you?
There was a problem hiding this comment.
I implemented Paolo's idea of encoding it as a byte and putting it in front of the state, let me know if that looks ok.
|
|
||
| instance ToSchema ConversationPagingTable where | ||
| schema = | ||
| (S.schema . description ?~ "Used getting PagedConv") $ |
There was a problem hiding this comment.
I do not understand/parse this description. Can you expand it?
There was a problem hiding this comment.
A left over rename and missing 'for', I will rewrite this, thanks 👍
| let remainingSize = fromRange size - fromIntegral (length (Public.pageConvIds localPage)) | ||
| if Public.pageHasMore localPage |
There was a problem hiding this comment.
I haven't looked at tests yet, but have you covered different cases that depend on the remaining size and if there is more to list?
There was a problem hiding this comment.
I am not sure if I understand, can you please elaborate?
|
|
||
| localsAndRemotes :: Domain -> Maybe C.PagingState -> Range 1 1000 Int32 -> Galley Public.ConvIdsPage | ||
| localsAndRemotes localDomain pagingState size = do | ||
| localPage <- pageToConvIdPage Public.PagingLocals . fmap (`Qualified` localDomain) <$> Data.conversationIdsPageFrom zusr pagingState size |
There was a problem hiding this comment.
The remote counterpart to Data.conversationIdsPageFrom is Data.remoteConversationIdsFrom, right? If so, how about calling it Data.localConversationIdsFrom or similar?
There was a problem hiding this comment.
I renamed them to localConversationIdsPageFrom and remoteConversationIdsPageFrom, so we keep page and local or remote both.
| -- should get all them in 16 times. | ||
| foldM_ (getChunkedConvs 16 0 alice) Nothing [16, 15 .. 0 :: Int] | ||
|
|
||
| -- This test exists |
There was a problem hiding this comment.
Are you saying it is a duplicate and of which one?
There was a problem hiding this comment.
Maybe I started writing the comment and something distracted me 😅
|
|
||
| foldM_ (getChunkedConvs 16 0 alice) Nothing [4, 3, 2, 1, 0 :: Int] | ||
|
|
||
| -- | Gets chucked conversation ids given size of each chunk, size of the last |
There was a problem hiding this comment.
| -- | Gets chucked conversation ids given size of each chunk, size of the last | |
| -- | Gets chunked conversation ids given size of each chunk, size of the last |
pcapriotti
left a comment
There was a problem hiding this comment.
Looks great!
I would like the paging state token to be returned a single obviously opaque bytestring, instead of an object someone might be tempted to peer into, but it's not a big deal.
I've left a few minor comments and fixes.
| -- term storage as the bytestring format may change useless when schema of a | ||
| -- table changes or when cassandra is upgraded. |
There was a problem hiding this comment.
| -- term storage as the bytestring format may change useless when schema of a | |
| -- table changes or when cassandra is upgraded. | |
| -- term storage as the bytestring format may change when the schema of a | |
| -- table changes or when cassandra is upgraded. |
| <*> pageHasMore .= field "has_more" schema | ||
| <*> pagePagingState .= field "paging_state" schema | ||
|
|
||
| -- | TODO: Would be nice to not expose these details to clients |
There was a problem hiding this comment.
This could be done by encoding ConversationPagingTable as an extra bit (or byte) of cpsPagingState. I'd suggest doing this now, even though I guess doing it later won't technically break backwards compatibility.
| schema = | ||
| let addPagingStateDoc = | ||
| description | ||
| ?~ "optional, when not first page of the conversation ids will be returned.\ |
There was a problem hiding this comment.
Missing "specified", I think.
| listConversationIds :: | ||
| routes | ||
| :- Summary "Get all conversation IDs." | ||
| :> Description "To retrieve next page, a client must pass the paging_state returned by previous page." |
There was a problem hiding this comment.
| :> Description "To retrieve next page, a client must pass the paging_state returned by previous page." | |
| :> Description "To retrieve the next page, a client must pass the paging_state returned by the previous page." |
| schema = | ||
| objectWithDocModifier | ||
| "ConversationPagingState" | ||
| (description ?~ "Clients should treat this object as opque and not try to parse it.") |
There was a problem hiding this comment.
| (description ?~ "Clients should treat this object as opque and not try to parse it.") | |
| (description ?~ "Clients should treat this object as opaque and not try to parse it.") |
| getConversationIds :: UserId -> Maybe ConvId -> Maybe (Range 1 1000 Int32) -> Galley (Public.ConversationList ConvId) | ||
| getConversationIds zusr start msize = do | ||
| listConversationIdsUnqualified :: UserId -> Maybe ConvId -> Maybe (Range 1 1000 Int32) -> Galley (Public.ConversationList ConvId) | ||
| listConversationIdsUnqualified zusr start msize = do |
There was a problem hiding this comment.
How about converting this to use conversationIdsPageFrom, so we can remove the legacy functions and even the ResultSet type?
There was a problem hiding this comment.
I like the new name, I don't know if we can easily remove the ResultSet type. I will look into it, but let's do it in the next PR?
| localsAndRemotes localDomain pagingState size = do | ||
| localPage <- pageToConvIdPage Public.PagingLocals . fmap (`Qualified` localDomain) <$> Data.conversationIdsPageFrom zusr pagingState size | ||
| let remainingSize = fromRange size - fromIntegral (length (Public.pageConvIds localPage)) | ||
| if Public.pageHasMore localPage |
There was a problem hiding this comment.
Would it make sense to do something like:
| if Public.pageHasMore localPage | |
| if Public.pageHasMore localPage or remainingSize <= 0 |
here to avoid having to query the other table unnecessarily?
Do you have any ideas about this? I was thinking of just putting a slash or a dot between them. The table we control so that shouldn't be a problem and the paging state is anyway rolled into a base64 encoded string so slash or dot shouldn't collide. |
Co-authored-by: Paolo Capriotti <paolo@capriotti.io>
I would add a 0 or 1 byte at the beginning (or end) of the paging state bytestring, then convert the whole thing to base 64. This could be done by the Schema instance directly. |
Can you please take a look? I don't know if you meant I could parse it with the schema library, I just used attoparsec. |
No, that's pretty much what I had in mind 👍 I think |
More details: https://wearezeta.atlassian.net/wiki/spaces/CORE/pages/477660475/Federated+Conversations+Pagination
Checklist
make git-add-cassandra-schemato update the cassandra schema documentation.