Skip to content

Add new API to list paginated qualified conv ids#1686

Merged
akshaymankar merged 32 commits intodevelopfrom
akshaymankar/paginate-remote-convs
Aug 10, 2021
Merged

Add new API to list paginated qualified conv ids#1686
akshaymankar merged 32 commits intodevelopfrom
akshaymankar/paginate-remote-convs

Conversation

@akshaymankar
Copy link
Member

@akshaymankar akshaymankar commented Jul 28, 2021

More details: https://wearezeta.atlassian.net/wiki/spaces/CORE/pages/477660475/Federated+Conversations+Pagination

Checklist

  • The PR Title explains the impact of the change.
  • The PR description provides context as to why the change should occur and what the code contributes to that effect. This could also be a link to a JIRA ticket or a Github issue, if there is one.
  • If end-points have been added or changed: the endpoint / config-flag checklist (see Wire-employee only backend wiki page) has been followed.
  • If a schema migration has been added, I ran make git-add-cassandra-schema to update the cassandra schema documentation.
  • Section Unreleased of CHANGELOG.md contains the following bits of information:
    • A line with the title and number of the PR in one or more suitable sub-sections.
    • If /a: measures to be taken by instance operators.
    • If /a: list of cassandra migrations.
    • If public end-points have been changed or added: does nginz need upgrade?
    • If internal end-points have been added or changed: which services have to be deployed in a specific order?

@akshaymankar akshaymankar marked this pull request as draft July 28, 2021 15:06
@akshaymankar akshaymankar force-pushed the akshaymankar/paginate-remote-convs branch from cee2135 to 5cc51aa Compare August 2, 2021 13:35
@akshaymankar akshaymankar changed the title [WIP] Create new endpoint to get paginated conv ids Create new endpoint to get paginated conv ids Aug 3, 2021
@akshaymankar akshaymankar marked this pull request as ready for review August 3, 2021 13:31
@akshaymankar akshaymankar force-pushed the akshaymankar/paginate-remote-convs branch from 227743b to 5888386 Compare August 3, 2021 13:32
@akshaymankar akshaymankar changed the title Create new endpoint to get paginated conv ids Add new API to list paginated qualified conv ids Aug 3, 2021
Copy link
Contributor

@pcapriotti pcapriotti left a comment

Choose a reason for hiding this comment

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

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 + 1 trick, 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 ResultSet type.

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.

Comment on lines 558 to 561
-- 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

I imagine this is necessary because there is no way in cassandra to say (domain, id) > (start_domain, start_id)?

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

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)

Copy link
Contributor

Choose a reason for hiding this comment

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

But why not use pagingState instead and let the driver handle it? Or am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

This actually works 🎉

Comment on lines 571 to 572
nextPage <- toResultSet remainingMax <$> paginate Cql.selectUserRemoteConvsFromDomain (paramsP Quorum (usr, d) (remainingMax + 1))
pure $ nextPage {resultSetResult = resultSetResult domainPage <> resultSetResult nextPage}
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is no longer required, so maybe we can just retire this thing soon.

Copy link
Contributor

@mdimjasevic mdimjasevic left a comment

Choose a reason for hiding this comment

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

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.

akshaymankar and others added 6 commits August 4, 2021 13:33
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>
Co-authored-by: Marko Dimjašević <marko.dimjasevic@wire.com>
Copy link
Contributor

@mdimjasevic mdimjasevic left a comment

Choose a reason for hiding this comment

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

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you planning to take care of the TODO as part of this PR? If not, it should be a future work note.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yes, I forgot to ask y'all if you had any better ideas. Do you?

Copy link
Member Author

Choose a reason for hiding this comment

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

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") $
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not understand/parse this description. Can you expand it?

Copy link
Member Author

Choose a reason for hiding this comment

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

A left over rename and missing 'for', I will rewrite this, thanks 👍

Comment on lines 155 to 156
let remainingSize = fromRange size - fromIntegral (length (Public.pageConvIds localPage))
if Public.pageHasMore localPage
Copy link
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

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
Copy link
Contributor

Choose a reason for hiding this comment

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

The remote counterpart to Data.conversationIdsPageFrom is Data.remoteConversationIdsFrom, right? If so, how about calling it Data.localConversationIdsFrom or similar?

Copy link
Member Author

Choose a reason for hiding this comment

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

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you saying it is a duplicate and of which one?

Copy link
Member Author

Choose a reason for hiding this comment

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

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
-- | 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

Copy link
Contributor

@pcapriotti pcapriotti left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines 117 to 118
-- term storage as the bytestring format may change useless when schema of a
-- table changes or when cassandra is upgraded.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
-- 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
Copy link
Contributor

Choose a reason for hiding this comment

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

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.\
Copy link
Contributor

Choose a reason for hiding this comment

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

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."
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
:> 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.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
(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
Copy link
Contributor

Choose a reason for hiding this comment

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

How about converting this to use conversationIdsPageFrom, so we can remove the legacy functions and even the ResultSet type?

Copy link
Member Author

Choose a reason for hiding this comment

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

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to do something like:

Suggested change
if Public.pageHasMore localPage
if Public.pageHasMore localPage or remainingSize <= 0

here to avoid having to query the other table unnecessarily?

@akshaymankar
Copy link
Member Author

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.

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>
@pcapriotti
Copy link
Contributor

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.

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.

@akshaymankar
Copy link
Member Author

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.

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.

@pcapriotti
Copy link
Contributor

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 attoparsec is a bit overkill for this, I would have probably used binary. But it doesn't really matter.

@akshaymankar akshaymankar merged commit 8855f6d into develop Aug 10, 2021
@akshaymankar akshaymankar deleted the akshaymankar/paginate-remote-convs branch August 10, 2021 14:33
@fisx fisx mentioned this pull request Aug 13, 2021
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.

3 participants