-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Conversation
1f2cc30
to
31dd45f
Compare
Codecov Report
@@ 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 |
Only added to |
979114b
to
5d7b0b2
Compare
@sunnya97 What's the rationale for a separate |
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. |
baseapp/baseapp.go
Outdated
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 |
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.
Maybe it's bike-shedding, but queryRouter
is more readable
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.
don't think it's bikeshedding - we should stick to the CamelCase convention
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.
Addressed
baseapp/baseapp.go
Outdated
@@ -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]) |
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.
what's path[1]
?
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.
Added comment
baseapp/baseapp.go
Outdated
// "/custom" prefix for keeper queries | ||
querier := app.queryrouter.Route(path[1]) | ||
ctx := app.checkState.ctx | ||
resBytes, err := querier(ctx, path[2:], req) |
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.
what's path[2:]
?
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.
yeah let's add some more comments as to what the path is supposed to be
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.
Addressed.
|
||
// QueryRouter provides queryables for each query path. | ||
type QueryRouter interface { | ||
AddRoute(r string, h sdk.Querier) (rtr QueryRouter) |
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.
h - handler, right?
what's rtr
?
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.
router I think
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 the rtr
is fine personally
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 change r
--> route
and h
--> handler
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 is fine IMO
baseapp/queryrouter.go
Outdated
} | ||
|
||
type queryrouter struct { | ||
routes []queryroute |
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 make it a map[string]queryroute
?
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.
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
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.
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.
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.
Ok, switched
x/gov/client/rest/rest.go
Outdated
@@ -476,54 +478,35 @@ func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc { | |||
return | |||
} | |||
} | |||
if len(strNumLatest) != 0 { | |||
numLatest, err = strconv.ParseInt(strNumLatest, 10, 64) |
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.
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
}
x/gov/depositsvotes.go
Outdated
@@ -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) && |
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.
return voteA.Voter.Equals(voteB.Voter) && voteA.ProposalID == voteB.ProposalID && voteA.Option == voteB.Option
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.
Replaced
x/gov/depositsvotes.go
Outdated
// 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) && |
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.
return depositA.Depositer.Equals(depositB.Depositer) && depositA.ProposalID == depositB.ProposalID && depositA.Amount.IsEqual(depositB.Amount)
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.
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 |
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 looks wrong because I clearly can see an increment. We should make it clear that this func mutates store.
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.
Oops, good catch. Fixed.
b1bcbe4
to
31cd4af
Compare
baseapp/baseapp.go
Outdated
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]) |
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 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()), |
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.
Is there a ResponseQuery.Log where we can put the err.String() in? That's for nondeterministic debug info like this.
baseapp/queryrouter.go
Outdated
} | ||
|
||
// AddRoute - TODO add description | ||
func (rtr *queryrouter) AddRoute(r string, h sdk.Querier) QueryRouter { |
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.
sanity check duplicates
@@ -74,6 +74,12 @@ func (app *BaseApp) Router() Router { | |||
} | |||
return app.router | |||
} | |||
func (app *BaseApp) QueryRouter() QueryRouter { |
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.
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!
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 why we have the seal it. I just saw that the router was sealed, so I sealed the QueryRouter as well
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.
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.
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.
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 { |
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.
vs nil? What do we do for Empty() and Equals() if any are nil?
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.
Oh, forgot that they can be nil. Fixed.
x/gov/queryable.go
Outdated
|
||
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, ¶ms) |
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.
Would JSON be better? It would be double encoded but might still be easier to deal with.
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.
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).
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 should go for JSON here, encoding efficiency isn't too much of a concern.
Have some questions and suggestions but otherwise LGTM. |
1ad8b56
to
307fff1
Compare
@@ -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) { |
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 this be accessible from cosmos-sdk/client
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.
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. |
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 add a more detailed description on how Queriers
work. Otherwise please submit a separate issue
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.
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. |
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; concept ACK, left comments.
|
||
// QueryRouter provides queryables for each query path. | ||
type QueryRouter interface { | ||
AddRoute(r string, h sdk.Querier) (rtr QueryRouter) |
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 is fine IMO
@@ -74,6 +74,12 @@ func (app *BaseApp) Router() Router { | |||
} | |||
return app.router | |||
} | |||
func (app *BaseApp) QueryRouter() QueryRouter { |
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.
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 { |
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.
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) |
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.
Extra parentheses
types/account.go
Outdated
if bz.Empty() && bz2.Empty() { | ||
return true | ||
} | ||
return (bytes.Compare(bz.Bytes(), bz2.Bytes()) == 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.
Extra parentheses
x/gov/queryable.go
Outdated
} | ||
|
||
if proposal.GetStatus() == StatusDepositPeriod { | ||
return keeper.cdc.MustMarshalBinaryBare(EmptyTallyResult()), nil |
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.
(nit) Can we keep a result value and marshal once at the end? A bit cleaner.
continue | ||
} | ||
|
||
if validProposalStatus(status) { |
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 it isn't, can we throw an error instead of failing silently?
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.
That's how it knows whether its filtering for a status or not. An "empty filter status" returns false on validProposalStatus
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.
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 { |
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.
Is this expected? Might the proposal have been deleted? Maybe we should add a DeletedProposal
record or something so we can inform the client.
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.
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 { |
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.
Is this expected?
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.
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 { |
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.
Is this expected? Do we ever have a proposal but not votes?
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.
Same as above
fae83c5
to
5ae20d2
Compare
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.
utACK
docs/
)PENDING.md
that include links to the relevant issue or PR that most accurately describes the change.cmd/gaia
andexamples/
For Admin Use: