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

R4R: keeper custom queries #1918

Merged
merged 11 commits into from
Aug 22, 2018
Merged

R4R: keeper custom queries #1918

merged 11 commits into from
Aug 22, 2018

Conversation

sunnya97
Copy link
Member

@sunnya97 sunnya97 commented Aug 3, 2018

  • Linked to github-issue with discussion and accepted design OR link to spec that describes this work.
  • Updated all relevant documentation (docs/)
  • Updated all relevant code comments
  • Wrote tests
  • Added entries in PENDING.md that include links to the relevant issue or PR that most accurately describes the change.
  • Updated cmd/gaia and examples/

For Admin Use:

  • Added appropriate labels to PR (ex. wip, ready-for-review, docs)
  • Reviewers Assigned
  • Squashed all commits, uses message "Merge pull request #XYZ: [title]" (coding standards)

@codecov
Copy link

codecov bot commented Aug 6, 2018

Codecov Report

Merging #1918 into develop will decrease coverage by 1.63%.
The diff coverage is 6.63%.

@@            Coverage Diff             @@
##           develop   #1918      +/-   ##
==========================================
- Coverage    63.83%   62.2%   -1.64%     
==========================================
  Files          113     115       +2     
  Lines         6684    6869     +185     
==========================================
+ Hits          4267    4273       +6     
- Misses        2133    2312     +179     
  Partials       284     284

@sunnya97 sunnya97 requested a review from zramsay as a code owner August 6, 2018 09:09
@sunnya97
Copy link
Member Author

sunnya97 commented Aug 6, 2018

Only added to x/gov/rest for now to demonstrate. Will add to CLI in separate PR. Other modules should mimic.

@sunnya97 sunnya97 changed the title WIP: keeper custom queries R4R: keeper custom queries Aug 6, 2018
@sunnya97 sunnya97 requested a review from jaekwon August 6, 2018 09:11
@cwgoes
Copy link
Contributor

cwgoes commented Aug 8, 2018

@sunnya97 What's the rationale for a separate QueryRouter - could we just use the single Router and have default-empty query endpoints? Presumably we want module names to correspond to routes in the same way for both transaction and query handling.

@sunnya97
Copy link
Member Author

sunnya97 commented Aug 8, 2018

Maybe not all modules have queriers, so instead of declaring a querier that just throws, they could just not add their route to the queryrouter. Forcing them to implement an empty querier just because they have a msghandler seems like conflation of concerns.

Also, the router points to the handler. There's nothing actually saying that there needs to be a 1-to-1 mapping from handler to keeper. So not sure how to get to the querier from the handler.

Modules (keepers) don't have names yet. They will need to at some point, though, to do the new errors with strings.

db dbm.DB // common DB backend
cms sdk.CommitMultiStore // Main (uncached) state
router Router // handle any kind of message
queryrouter QueryRouter // router for redirecting query calls
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it's bike-shedding, but queryRouter is more readable

Copy link
Contributor

Choose a reason for hiding this comment

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

don't think it's bikeshedding - we should stick to the CamelCase convention

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed

@@ -362,6 +366,22 @@ func handleQueryP2P(app *BaseApp, path []string, req abci.RequestQuery) (res abc
return sdk.ErrUnknownRequest(msg).QueryResult()
}

func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) {
// "/custom" prefix for keeper queries
querier := app.queryrouter.Route(path[1])
Copy link
Contributor

Choose a reason for hiding this comment

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

what's path[1]?

Copy link
Member Author

@sunnya97 sunnya97 Aug 15, 2018

Choose a reason for hiding this comment

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

Added comment

// "/custom" prefix for keeper queries
querier := app.queryrouter.Route(path[1])
ctx := app.checkState.ctx
resBytes, err := querier(ctx, path[2:], req)
Copy link
Contributor

Choose a reason for hiding this comment

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

what's path[2:]?

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah let's add some more comments as to what the path is supposed to be

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed.


// QueryRouter provides queryables for each query path.
type QueryRouter interface {
AddRoute(r string, h sdk.Querier) (rtr QueryRouter)
Copy link
Contributor

Choose a reason for hiding this comment

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

h - handler, right?

what's rtr?

Copy link
Contributor

Choose a reason for hiding this comment

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

router I think

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the rtr is fine personally

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd change r --> route and h --> handler

Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine IMO

}

type queryrouter struct {
routes []queryroute
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 make it a map[string]queryroute?

Copy link
Member Author

Choose a reason for hiding this comment

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

Idk, this is the way the normal router is, and so I just copied that. I think it may have had to do with allowing for more complex routes in the future (like overriding "gov/proposal" to go somewhere different thatn "gov/*"). @jaekwon

Copy link
Contributor

Choose a reason for hiding this comment

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

Mmm I'd be fine w/ either map[string]queryroute or not, I think QueryRouter might/should be simpler. But either way duplicate registration should panic.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, switched

@@ -476,54 +478,35 @@ func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc {
return
}
}
if len(strNumLatest) != 0 {
numLatest, err = strconv.ParseInt(strNumLatest, 10, 64)
Copy link
Contributor

@melekes melekes Aug 14, 2018

Choose a reason for hiding this comment

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

Would be good to create a helper function. I am sure this is not the only case where we parse int64. No?

func parseInt64OrReturnBadRequest(s string, w http. ResponseWriter) (n int64, ok bool) {
  var err error
  n, err = strconv.ParseInt(s, 10, 64)
  if err != nil {
    w.WriteHeader(http.StatusBadRequest)
    err := fmt.Errorf("'%s' is not a valid int64", s)
    w.Write([]byte(err.Error()))
    return 0, false
  }
  return n, true
}

@@ -15,13 +15,45 @@ type Vote struct {
Option VoteOption `json:"option"` // option from OptionSet chosen by the voter
}

// Returns whether 2 votes are equal
func (voteA Vote) Equals(voteB Vote) bool {
if voteA.Voter.Equals(voteB.Voter) &&
Copy link
Contributor

Choose a reason for hiding this comment

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

return voteA.Voter.Equals(voteB.Voter) && voteA.ProposalID == voteB.ProposalID && voteA.Option == voteB.Option

Copy link
Member Author

Choose a reason for hiding this comment

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

Replaced

// Deposit
type Deposit struct {
Depositer sdk.AccAddress `json:"depositer"` // Address of the depositer
ProposalID int64 `json:"proposal_id"` // proposalID of the proposal
Amount sdk.Coins `json:"amount"` // Deposit amount
}

// Returns whether 2 deposits are equal
func (depositA Deposit) Equals(depositB Deposit) bool {
if depositA.Depositer.Equals(depositB.Depositer) &&
Copy link
Contributor

Choose a reason for hiding this comment

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

return depositA.Depositer.Equals(depositB.Depositer) && depositA.ProposalID == depositB.ProposalID && depositA.Amount.IsEqual(depositB.Amount)

Copy link
Member Author

Choose a reason for hiding this comment

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

Replaced

@@ -131,6 +178,19 @@ func (keeper Keeper) getNewProposalID(ctx sdk.Context) (proposalID int64, err sd
return proposalID, nil
}

// Peeks the next available ProposalID without incrementing it
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks wrong because I clearly can see an increment. We should make it clear that this func mutates store.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, good catch. Fixed.

func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) {
// path[0] should be "custom" because "/custom" prefix is required for keeper queries.
// the queryRouter routes using path[1]. For example, in the path "custom/gov/proposal", queryRouter routes using "gov"
querier := app.queryRouter.Route(path[1])
Copy link
Contributor

Choose a reason for hiding this comment

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

If it doesn't exist, a useful error message...

resBytes, err := querier(ctx, path[2:], req)
if err != nil {
return abci.ResponseQuery{
Code: uint32(err.ABCICode()),
Copy link
Contributor

@jaekwon jaekwon Aug 16, 2018

Choose a reason for hiding this comment

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

Is there a ResponseQuery.Log where we can put the err.String() in? That's for nondeterministic debug info like this.

}

// AddRoute - TODO add description
func (rtr *queryrouter) AddRoute(r string, h sdk.Querier) QueryRouter {
Copy link
Contributor

Choose a reason for hiding this comment

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

sanity check duplicates

@@ -74,6 +74,12 @@ func (app *BaseApp) Router() Router {
}
return app.router
}
func (app *BaseApp) QueryRouter() QueryRouter {
Copy link
Contributor

Choose a reason for hiding this comment

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

What does sealing do on the baseapp and do we need to seal the query router as well? anything else we're missing? docs would be helpful!

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'm not sure why we have the seal it. I just saw that the router was sealed, so I sealed the QueryRouter as well

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I think we should restrict sealing to fields that actually need to be sealed for clarity, otherwise readers of the code will have a hard time figuring out what our intent was (as apparently we ourselves have a hard time figuring out already!). As I understand, this is just defensive coding - I believe @mossid wrote the original PR - and we only need to do it for write-once fields or fields that should not be read from after a certain event in the app lifecycle.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, don't we want to query the QueryRouter after the app is sealed?

types/account.go Outdated
}

// Returns boolean for whether an AccAddress is empty
func (bz ValAddress) Empty() bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

vs nil? What do we do for Empty() and Equals() if any are nil?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, forgot that they can be nil. Fixed.


func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
var params QueryProposalParams
err2 := keeper.cdc.UnmarshalBinary(req.Data, &params)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would JSON be better? It would be double encoded but might still be easier to deal with.

Copy link
Member Author

@sunnya97 sunnya97 Aug 17, 2018

Choose a reason for hiding this comment

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

Yeah, it might be easier to encode for clients, but yeah, then it would be a JSON-encoded blob inside a amino-encoded blob (the QueryRequest).

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should go for JSON here, encoding efficiency isn't too much of a concern.

@jaekwon
Copy link
Contributor

jaekwon commented Aug 16, 2018

Have some questions and suggestions but otherwise LGTM.

@@ -100,3 +102,15 @@ func signAndBuild(w http.ResponseWriter, cliCtx context.CLIContext, baseReq base

w.Write(output)
}

func parseInt64OrReturnBadRequest(s string, w http.ResponseWriter) (n int64, ok bool) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be accessible from cosmos-sdk/client instead ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Most of this util.go file should move to there, but I'll do that in a separate PR.

@@ -6,7 +6,7 @@

The Cosmos SDK has all the necessary pre-built modules to add functionality on top of a `BaseApp`, which is the template to build a blockchain dApp in Cosmos. In this context, a `module` is a fundamental unit in the Cosmos SDK.

Each module is an extension of the `BaseApp`'s functionalities that defines transactions, handles application state and manages the state transition logic. Each module also contains handlers for messages and transactions, as well as REST and CLI for secure user interactions.
Each module is an extension of the `BaseApp`'s functionalities that defines transactions, handles application state and manages the state transition logic. Each module also contains handlers for messages and transactions, queriers for handling query requests, as well as REST and CLI for secure user interactions.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd add a more detailed description on how Queriers work. Otherwise please submit a separate issue

Copy link
Member Author

Choose a reason for hiding this comment

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

@mossid
Copy link
Contributor

mossid commented Aug 20, 2018

Wouldn't it be better to unmarshal req.Data to interface{} in baseapp and type match in queryhandler, just like as normal handler does? Or, we can type assert in each of the subpath.

Copy link
Contributor

@cwgoes cwgoes left a comment

Choose a reason for hiding this comment

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

Thanks; concept ACK, left comments.


// QueryRouter provides queryables for each query path.
type QueryRouter interface {
AddRoute(r string, h sdk.Querier) (rtr QueryRouter)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine IMO

@@ -74,6 +74,12 @@ func (app *BaseApp) Router() Router {
}
return app.router
}
func (app *BaseApp) QueryRouter() QueryRouter {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I think we should restrict sealing to fields that actually need to be sealed for clarity, otherwise readers of the code will have a hard time figuring out what our intent was (as apparently we ourselves have a hard time figuring out already!). As I understand, this is just defensive coding - I believe @mossid wrote the original PR - and we only need to do it for write-once fields or fields that should not be read from after a certain event in the app lifecycle.

@@ -74,6 +74,12 @@ func (app *BaseApp) Router() Router {
}
return app.router
}
func (app *BaseApp) QueryRouter() QueryRouter {
Copy link
Contributor

Choose a reason for hiding this comment

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

Also, don't we want to query the QueryRouter after the app is sealed?

types/account.go Outdated
return true
}
bz2 := AccAddress{}
return (bytes.Compare(bz.Bytes(), bz2.Bytes()) == 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Extra parentheses

types/account.go Outdated
if bz.Empty() && bz2.Empty() {
return true
}
return (bytes.Compare(bz.Bytes(), bz2.Bytes()) == 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Extra parentheses

}

if proposal.GetStatus() == StatusDepositPeriod {
return keeper.cdc.MustMarshalBinaryBare(EmptyTallyResult()), nil
Copy link
Contributor

Choose a reason for hiding this comment

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

(nit) Can we keep a result value and marshal once at the end? A bit cleaner.

continue
}

if validProposalStatus(status) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If it isn't, can we throw an error instead of failing silently?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's how it knows whether its filtering for a status or not. An "empty filter status" returns false on validProposalStatus

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah hmm, it would still be nice to be explicit but I guess it's OK for now.

}

proposal := keeper.GetProposal(ctx, proposalID)
if proposal == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this expected? Might the proposal have been deleted? Maybe we should add a DeletedProposal record or something so we can inform the client.

Copy link
Member Author

Choose a reason for hiding this comment

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

Only proposals that don't make it to the voting period (not get enough deposit) get deleted from state. Don't think its necessary to add DeletedProposal record to state for them.


if depositerAddr != nil && len(depositerAddr) != 0 {
_, found := keeper.GetDeposit(ctx, proposalID, depositerAddr)
if !found {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this expected?

Copy link
Member Author

Choose a reason for hiding this comment

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

Basically, this is for filtering. In this case, depositerAddr is what you're trying to filter by. If it is empty, that means you're not trying to filter by that.

for proposalID := maxProposalID - numLatest; proposalID < maxProposalID; proposalID++ {
if voterAddr != nil && len(voterAddr) != 0 {
_, found := keeper.GetVote(ctx, proposalID, voterAddr)
if !found {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this expected? Do we ever have a proposal but not votes?

Copy link
Member Author

Choose a reason for hiding this comment

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

Same as above

Copy link
Contributor

@cwgoes cwgoes left a comment

Choose a reason for hiding this comment

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

utACK

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.

7 participants