-
Notifications
You must be signed in to change notification settings - Fork 440
Pagination for /wallet/addresses and /wallet/transactions #2511
base: master
Are you sure you want to change the base?
Conversation
I think it makes more sense to have Is it possible to specify how many results are included in each response instead of hard-coding it at 25? |
Most pagination APIs don't require you to send page numbers, but rather item numbers. You provide This is more flexible and lets you have multiple ways of "paging":
|
It's also standard to include links to the next/previous pages in the header of the response. Here's an example from the GitHub API: https://developer.github.com/v3/guides/traversing-with-pagination. We don't have to add those now, but it's a good thing to keep in mind. |
@DavidVorick as the reporter of #1478 do you have a preference on having the pagination delineate by total pages or item numbers? It seems there are pros/cons to both ideas and either would probably work fine. |
Cool, I've seen pagination done both ways. But yeah the one that specifies the start might be better. |
sample response for
|
Also, I'm not understanding the tests. I think the failure are unrelated to my changes? |
Hey, sorry to let this go for two weeks without a response. If you rebase to the current master and push, some of the test failures should disappear. The master branch has been in an unstable state for a while, but I think that we've finally cleaned it up. With regards to the pagination, my preference would be to have a starting item and a maximum number of entries to return. Let's keep it simple, and just start with that for now, instead of also returning links for 'next' and 'prev'. |
api/api.go
Outdated
|
||
"github.com/NebulousLabs/Sia/build" | ||
"github.com/NebulousLabs/Sia/modules" | ||
) | ||
|
||
const ( | ||
// size of each page for paginated API responses | ||
DEFAULT_PAGINATION_SIZE = 25 |
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.
We can probably default to something more like 1000. Also, convention is to put constants like this in consts.go
, and the naming scheme would be DefaultPaginationSize
instead of using all caps and underscores.
api/api.go
Outdated
PaginationQueryIsValid bool | ||
Start int | ||
Limit int | ||
} |
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.
need to run go fmt
here. If you don't run go fmt
then the build will always fail.
Best thing to do though is to run make test-long
before you push code, because that'll do all the formatting, linting, vetting, and of course testing for you. Takes about 10 minutes on a desktop w/SSD.
have to run for a call, will continue review later. All functions need docstrings that explain what they do. This code overall is not very well commented. |
@hudaniel I would recommend becoming familiar with this document for naming conventions and Sia developer tips. Thanks for your work so far, we're excited to see this PR get merged once it's complete. https://github.com/NebulousLabs/Sia/blob/master/doc/Developers.md |
Thanks for the review, I will address these comments after the holidays! |
@hudaniel Any chance we can get you to revive this PR? I know several people are still interested in the feature and you have got the best chance of pushing it through. |
Hey, sorry forgot about this haha. Yeah I’ll have some time this weekend to
get this out.
…On Thu, Feb 22, 2018 at 3:01 PM Thomas Bennett ***@***.***> wrote:
@hudaniel <https://github.com/hudaniel> Any chance we can get you to
revive this PR? I know several people are still interested in the feature
and you have got the best chance of pushing it through.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#2511 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABpyaBEyJR5kYKyaNb7EKLUpssra7WNDks5tXfHegaJpZM4Q5Xj2>
.
|
updated @tbenz9 @DavidVorick |
Thanks, we will be looking at this as soon as possible. |
Thanks for doing this work! In my Sia load test, I'm seeing siad latency reaching up to 300+ seconds, so pagination is a welcome feature. An issue with this implementation is that it doesn't protect the client against changes to the underlying data between paginated calls. This doesn't feel like such a big deal with transactions because transactions can't be deleted and the API guarantees chronological order, but consider if we apply this to something like Imagine this scenario:
What does the client do in that situation? Do they have to throw everything away and start the query over? If the underlying data is changing more frequently than the client can iterate through the total pages, the client would never finish a query. APIs usually handle this with page tokens. BigQuery does this, for example. Instead of the server reporting the state at the time of each page call, they give the client a token associated with a set of immutable results. Even if the underlying data changes, the server will let the client iterate through the results snapshotted at the time when the page token was created. Relatedly, it would be nice to allow the client to do a "dry" query where they request no data except the page token. My reading of the current implementation is that the client can't get metadata about the total size of the list unless they request at least one element of data. This is awkward because what most clients want to do is something like: token := getPageToken(query)
while token.hasMoreElements() {
results = append(results, getNextPage(token))
} |
@mtlynch That's a nice property to have, but the implementation would be difficult. Could tokens be tacked on afterward without breaking compatibility? I'm also wondering if pagination is really required for wallet transactions. You can already query them by block height, so shouldn't the client be able to handle the pagination? There also isn't an issue of reordering/invalidation, because new transactions should only be added to the end of the list. |
"All wallet calls which return potentially large amounts of data (like the transactions or the addresses) need to support pagination in the API." --David Vorick in #1478 |
Would it be possible to make a page token, but just have it get invalidated any time an element is added or removed in the underlying data? That way, the server doesn't have to maintain snapshots, but the client knows when they need to re-start a paged query because the data shifted. It should be backwards compatible from the client's end because they have to handle page token expiration anyway. If we later improve the server so that it keeps page tokens (and their associated snapshots) valid for 10 mins, then the clients don't have to change their code, but they'll encounter expired tokens much less frequently. It still has the problem that the client can't achieve a single, consistent "view" of the underlying data, but that doesn't seem critical. |
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.
Thanks for implementing this! I'd just like to see some refactoring to simplify the code.The functionality looks fine to me at a first glance but we still need tests for each endpoint that supports pagination in our new siatest
package.
@@ -29,6 +32,25 @@ type Error struct { | |||
// be valid or invalid depending on the current state of a module. | |||
} | |||
|
|||
type PaginationRequest struct { |
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 think we don't need that struct at all. We can just return multiple return values in the methods that use it.
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.
Btw all exported structs and methods need a comment.
Limit: paginationRequest.Limit, | ||
TotalPages: totalPages, | ||
} | ||
return pagination, paginationResponse |
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.
pagination
and paginationResponse
have exactly the same fields. Why not just do a
return startPageIndex, endingPageIndex, totalPages? That reduces the length of the method by 50%. You might even want to use named return values instead. That way it is just a
return` at the end of the function.
build.Critical("pagination request limit must be greater than 0") | ||
} | ||
slice := reflect.ValueOf(sliceInterface) | ||
if slice.Kind() != reflect.Slice { |
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.
This seems to be a bit complicated for just getting the length of arbitrary data. Why not just change sliceInterface interface{}
to dataLen int
?
// returns the starting index, end index, and total pages of a given slice for the page | ||
func GetPaginationIndicesAndResponse(sliceInterface interface{}, paginationRequest PaginationRequest) (PaginationWrapper, PaginationResponse) { | ||
if paginationRequest.Limit <= 0 { | ||
build.Critical("pagination request limit must be greater than 0") |
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.
build.Critical
should be used as a last resort. In production this will only print to the terminal and will continue the execution of the program. In a debug build this panics.
You should rather return an errors.New("pagination request limit must be greater than 0")
and write that error to the http response later.
startingPageIndex := int(math.Min(float64(paginationRequest.Start), float64(slice.Len()))) | ||
endingPageIndex := int(math.Min(float64(paginationRequest.Start+paginationRequest.Limit), float64(slice.Len()))) | ||
totalPages := slice.Len() / paginationRequest.Limit | ||
if math.Mod(float64(slice.Len()), float64(paginationRequest.Limit)) != 0 { |
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.
You probably don't need math.Mod
with all the float casting. Instead you can do if slice.Len() % paginationRequest.Limit != 0
@@ -162,3 +184,77 @@ func WriteJSON(w http.ResponseWriter, obj interface{}) { | |||
func WriteSuccess(w http.ResponseWriter) { | |||
w.WriteHeader(http.StatusNoContent) | |||
} | |||
|
|||
func GetPaginationDefaultRequest(req *http.Request) PaginationRequest { | |||
return GetPaginationRequest(req, "start", "limit") |
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'm not sure about the custom names for start
and limit
. If we do that, the names of the fields of PaginationResponse
should probably also be custom to match the fields in the request.
End int | ||
TotalPages int | ||
} | ||
|
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.
There are probably enough new pagination types and methods to move the generic ones into a node/api/pagination.go
file instead of having them in api.go
.
The ones that are specific to a module can still be in the corresponding [module].go
file.
Addresses: addresses, | ||
Pagination: paginationResponse, | ||
}) | ||
} |
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 mentioned above I would change the call to GetPaginationRequest
so that you get something like the following:
start, limit, err := GetPaginationDefaultRequest(req)
...
if err == errNoPagination {
// legacy behavior
return
}
// new behavior
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.
You can create a custom error like errNoPagination
by declaring it at the top of the file. You can find an example in modules/transactionpool/transactionpool.go
where we declare errNilCS
and errNilGateway
at the top.
Of course that also means you have to do a return errNoPagination
in GetPaginationRequest
if no params were specified.
ConfirmedTransactions: confirmedTxns, | ||
UnconfirmedTransactions: unconfirmedTxns, | ||
}) | ||
} else { |
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.
We try to avoid nested if
statements. You can get rid of the else
by returning after the call to WriteJSON
. In the API we usually don't do anything after calling WriteJSON
or WriteError
which means you can usually simplify your code by returning right afterwards.
@@ -49,6 +49,9 @@ var ( | |||
SiafundCount = NewCurrency64(10000) | |||
SiafundPortion = big.NewRat(39, 1000) | |||
TargetWindow BlockHeight | |||
|
|||
// size of each page for paginated API responses | |||
DefaultPaginationSize = 50 |
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.
We usually try to keep the constants in a consts.go
file within the module it is used in. Since it is only 1 declaration you can probably add defaultPaginationSize
to a node/api/pagination.go
file together with the code or move it into a node/api/consts.go
file.
New to golang. Please let me know what could be improved.
Addressing this Issue: #1478
Changes:
Adding an optional
page
query to/wallet/addresses
and optional queriesconfirmPage
andunconfirmedPage
to/wallet/transactions
. Not specifying these queries will return the same response as before to better support the command line tools.The response of these endpoints given the page queries will reply with a
total_pages
field which will tell the client how many pages are left, beginning with 0.