mirror of
https://github.com/fluencelabs/tendermint
synced 2025-04-25 14:52:17 +00:00
## PR This PR introduces a fundamental breaking change to the structure of ABCI response and tx tags and the way they're processed. Namely, the SDK can support more complex and aggregated events for distribution and slashing. In addition, block responses can include duplicate keys in events. Implement new Event type. An event has a type and a list of KV pairs (ie. list-of-lists). Typical events may look like: "rewards": [{"amount": "5000uatom", "validator": "...", "recipient": "..."}] "sender": [{"address": "...", "balance": "100uatom"}] The events are indexed by {even.type}.{even.attribute[i].key}/.... In this case a client would subscribe or query for rewards.recipient='...' ABCI response types and related types now include Events []Event instead of Tags []cmn.KVPair. PubSub logic now publishes/matches against map[string][]string instead of map[string]string to support duplicate keys in response events (from #1385). A match is successful if the value is found in the slice of strings. closes: #1859 closes: #2905 ## Commits: * Implement Event ABCI type and updates responses to use events * Update messages_test.go * Update kvstore.go * Update event_bus.go * Update subscription.go * Update pubsub.go * Update kvstore.go * Update query logic to handle slice of strings in events * Update Empty#Matches and unit tests * Update pubsub logic * Update EventBus#Publish * Update kv tx indexer * Update godocs * Update ResultEvent to use slice of strings; update RPC * Update more tests * Update abci.md * Check for key in validateAndStringifyEvents * Fix KV indexer to skip empty keys * Fix linting errors * Update CHANGELOG_PENDING.md * Update docs/spec/abci/abci.md Co-Authored-By: Federico Kunze <31522760+fedekunze@users.noreply.github.com> * Update abci/types/types.proto Co-Authored-By: Ethan Buchman <ethan@coinculture.info> * Update docs/spec/abci/abci.md Co-Authored-By: Ethan Buchman <ethan@coinculture.info> * Update libs/pubsub/query/query.go Co-Authored-By: Ethan Buchman <ethan@coinculture.info> * Update match function to match if ANY value matches * Implement TestSubscribeDuplicateKeys * Update TestMatches to include multi-key test cases * Update events.go * Update Query interface godoc * Update match godoc * Add godoc for matchValue * DRY-up tx indexing * Return error from PublishWithEvents in EventBus#Publish * Update PublishEventNewBlockHeader to return an error * Fix build * Update events doc in ABCI * Update ABCI events godoc * Implement TestEventBusPublishEventTxDuplicateKeys * Update TestSubscribeDuplicateKeys to be table-driven * Remove mod file * Remove markdown from events godoc * Implement TestTxSearchDeprecatedIndexing test
240 lines
6.9 KiB
Go
240 lines
6.9 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
cmn "github.com/tendermint/tendermint/libs/common"
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
tmpubsub "github.com/tendermint/tendermint/libs/pubsub"
|
|
tmquery "github.com/tendermint/tendermint/libs/pubsub/query"
|
|
nm "github.com/tendermint/tendermint/node"
|
|
"github.com/tendermint/tendermint/rpc/core"
|
|
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
|
rpctypes "github.com/tendermint/tendermint/rpc/lib/types"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
/*
|
|
Local is a Client implementation that directly executes the rpc
|
|
functions on a given node, without going through HTTP or GRPC.
|
|
|
|
This implementation is useful for:
|
|
|
|
* Running tests against a node in-process without the overhead
|
|
of going through an http server
|
|
* Communication between an ABCI app and Tendermint core when they
|
|
are compiled in process.
|
|
|
|
For real clients, you probably want to use client.HTTP. For more
|
|
powerful control during testing, you probably want the "client/mock" package.
|
|
|
|
You can subscribe for any event published by Tendermint using Subscribe method.
|
|
Note delivery is best-effort. If you don't read events fast enough, Tendermint
|
|
might cancel the subscription. The client will attempt to resubscribe (you
|
|
don't need to do anything). It will keep trying indefinitely with exponential
|
|
backoff (10ms -> 20ms -> 40ms) until successful.
|
|
*/
|
|
type Local struct {
|
|
*types.EventBus
|
|
Logger log.Logger
|
|
ctx *rpctypes.Context
|
|
}
|
|
|
|
// NewLocal configures a client that calls the Node directly.
|
|
//
|
|
// Note that given how rpc/core works with package singletons, that
|
|
// you can only have one node per process. So make sure test cases
|
|
// don't run in parallel, or try to simulate an entire network in
|
|
// one process...
|
|
func NewLocal(node *nm.Node) *Local {
|
|
node.ConfigureRPC()
|
|
return &Local{
|
|
EventBus: node.EventBus(),
|
|
Logger: log.NewNopLogger(),
|
|
ctx: &rpctypes.Context{},
|
|
}
|
|
}
|
|
|
|
var _ Client = (*Local)(nil)
|
|
|
|
// SetLogger allows to set a logger on the client.
|
|
func (c *Local) SetLogger(l log.Logger) {
|
|
c.Logger = l
|
|
}
|
|
|
|
func (c *Local) Status() (*ctypes.ResultStatus, error) {
|
|
return core.Status(c.ctx)
|
|
}
|
|
|
|
func (c *Local) ABCIInfo() (*ctypes.ResultABCIInfo, error) {
|
|
return core.ABCIInfo(c.ctx)
|
|
}
|
|
|
|
func (c *Local) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) {
|
|
return c.ABCIQueryWithOptions(path, data, DefaultABCIQueryOptions)
|
|
}
|
|
|
|
func (c *Local) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) {
|
|
return core.ABCIQuery(c.ctx, path, data, opts.Height, opts.Prove)
|
|
}
|
|
|
|
func (c *Local) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) {
|
|
return core.BroadcastTxCommit(c.ctx, tx)
|
|
}
|
|
|
|
func (c *Local) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) {
|
|
return core.BroadcastTxAsync(c.ctx, tx)
|
|
}
|
|
|
|
func (c *Local) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) {
|
|
return core.BroadcastTxSync(c.ctx, tx)
|
|
}
|
|
|
|
func (c *Local) UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) {
|
|
return core.UnconfirmedTxs(c.ctx, limit)
|
|
}
|
|
|
|
func (c *Local) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) {
|
|
return core.NumUnconfirmedTxs(c.ctx)
|
|
}
|
|
|
|
func (c *Local) NetInfo() (*ctypes.ResultNetInfo, error) {
|
|
return core.NetInfo(c.ctx)
|
|
}
|
|
|
|
func (c *Local) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) {
|
|
return core.DumpConsensusState(c.ctx)
|
|
}
|
|
|
|
func (c *Local) ConsensusState() (*ctypes.ResultConsensusState, error) {
|
|
return core.ConsensusState(c.ctx)
|
|
}
|
|
|
|
func (c *Local) Health() (*ctypes.ResultHealth, error) {
|
|
return core.Health(c.ctx)
|
|
}
|
|
|
|
func (c *Local) DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) {
|
|
return core.UnsafeDialSeeds(c.ctx, seeds)
|
|
}
|
|
|
|
func (c *Local) DialPeers(peers []string, persistent bool) (*ctypes.ResultDialPeers, error) {
|
|
return core.UnsafeDialPeers(c.ctx, peers, persistent)
|
|
}
|
|
|
|
func (c *Local) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) {
|
|
return core.BlockchainInfo(c.ctx, minHeight, maxHeight)
|
|
}
|
|
|
|
func (c *Local) Genesis() (*ctypes.ResultGenesis, error) {
|
|
return core.Genesis(c.ctx)
|
|
}
|
|
|
|
func (c *Local) Block(height *int64) (*ctypes.ResultBlock, error) {
|
|
return core.Block(c.ctx, height)
|
|
}
|
|
|
|
func (c *Local) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) {
|
|
return core.BlockResults(c.ctx, height)
|
|
}
|
|
|
|
func (c *Local) Commit(height *int64) (*ctypes.ResultCommit, error) {
|
|
return core.Commit(c.ctx, height)
|
|
}
|
|
|
|
func (c *Local) Validators(height *int64) (*ctypes.ResultValidators, error) {
|
|
return core.Validators(c.ctx, height)
|
|
}
|
|
|
|
func (c *Local) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) {
|
|
return core.Tx(c.ctx, hash, prove)
|
|
}
|
|
|
|
func (c *Local) TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) {
|
|
return core.TxSearch(c.ctx, query, prove, page, perPage)
|
|
}
|
|
|
|
func (c *Local) Subscribe(ctx context.Context, subscriber, query string, outCapacity ...int) (out <-chan ctypes.ResultEvent, err error) {
|
|
q, err := tmquery.New(query)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse query")
|
|
}
|
|
sub, err := c.EventBus.Subscribe(ctx, subscriber, q)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to subscribe")
|
|
}
|
|
|
|
outCap := 1
|
|
if len(outCapacity) > 0 {
|
|
outCap = outCapacity[0]
|
|
}
|
|
|
|
outc := make(chan ctypes.ResultEvent, outCap)
|
|
go c.eventsRoutine(sub, subscriber, q, outc)
|
|
|
|
return outc, nil
|
|
}
|
|
|
|
func (c *Local) eventsRoutine(sub types.Subscription, subscriber string, q tmpubsub.Query, outc chan<- ctypes.ResultEvent) {
|
|
for {
|
|
select {
|
|
case msg := <-sub.Out():
|
|
result := ctypes.ResultEvent{Query: q.String(), Data: msg.Data(), Events: msg.Events()}
|
|
if cap(outc) == 0 {
|
|
outc <- result
|
|
} else {
|
|
select {
|
|
case outc <- result:
|
|
default:
|
|
c.Logger.Error("wanted to publish ResultEvent, but out channel is full", "result", result, "query", result.Query)
|
|
}
|
|
}
|
|
case <-sub.Cancelled():
|
|
if sub.Err() == tmpubsub.ErrUnsubscribed {
|
|
return
|
|
}
|
|
|
|
c.Logger.Error("subscription was cancelled, resubscribing...", "err", sub.Err(), "query", q.String())
|
|
sub = c.resubscribe(subscriber, q)
|
|
if sub == nil { // client was stopped
|
|
return
|
|
}
|
|
case <-c.Quit():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to resubscribe with exponential backoff.
|
|
func (c *Local) resubscribe(subscriber string, q tmpubsub.Query) types.Subscription {
|
|
attempts := 0
|
|
for {
|
|
if !c.IsRunning() {
|
|
return nil
|
|
}
|
|
|
|
sub, err := c.EventBus.Subscribe(context.Background(), subscriber, q)
|
|
if err == nil {
|
|
return sub
|
|
}
|
|
|
|
attempts++
|
|
time.Sleep((10 << uint(attempts)) * time.Millisecond) // 10ms -> 20ms -> 40ms
|
|
}
|
|
}
|
|
|
|
func (c *Local) Unsubscribe(ctx context.Context, subscriber, query string) error {
|
|
q, err := tmquery.New(query)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to parse query")
|
|
}
|
|
return c.EventBus.Unsubscribe(ctx, subscriber, q)
|
|
}
|
|
|
|
func (c *Local) UnsubscribeAll(ctx context.Context, subscriber string) error {
|
|
return c.EventBus.UnsubscribeAll(ctx, subscriber)
|
|
}
|