mirror of
https://github.com/tendermint/tendermint.git
synced 2026-02-03 02:22:04 +00:00
* Performance improvements for the event query API (#7319) Rework the implementation of event query parsing and execution to improve performance and reduce memory usage. Previous memory and CPU profiles of the pubsub service showed query processing as a significant hotspot. While we don't have evidence that this is visibly hurting users, fixing it is fairly easy and self-contained. Updates #6439. Typical benchmark results comparing the original implementation (PEG) with the reworked implementation (Custom): ``` TEST TIME/OP BYTES/OP ALLOCS/OP SPEEDUP MEM SAVING BenchmarkParsePEG-12 51716 ns 526832 27 BenchmarkParseCustom-12 2167 ns 4616 17 23.8x 99.1% BenchmarkMatchPEG-12 3086 ns 1097 22 BenchmarkMatchCustom-12 294.2 ns 64 3 10.5x 94.1% ```
408 lines
9.6 KiB
Go
408 lines
9.6 KiB
Go
package query_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tendermint/tendermint/abci/types"
|
|
"github.com/tendermint/tendermint/libs/pubsub"
|
|
"github.com/tendermint/tendermint/libs/pubsub/query"
|
|
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
|
|
)
|
|
|
|
var _ pubsub.Query = (*query.Query)(nil)
|
|
|
|
// Example events from the OpenAPI documentation:
|
|
//
|
|
// https://github.com/tendermint/tendermint/blob/master/rpc/openapi/openapi.yaml
|
|
//
|
|
// Redactions:
|
|
//
|
|
// - Add an explicit "tm" event for the built-in attributes.
|
|
// - Remove Index fields (not relevant to tests).
|
|
// - Add explicit balance values (to use in tests).
|
|
var apiEvents = map[string][]string{
|
|
"tm.event": {
|
|
"Tx",
|
|
},
|
|
"tm.hash": {
|
|
"XYZ",
|
|
},
|
|
"tm.height": {
|
|
"5",
|
|
},
|
|
"rewards.withdraw.address": {
|
|
"AddrA",
|
|
"AddrB",
|
|
},
|
|
"rewards.withdraw.source": {
|
|
"SrcX",
|
|
"SrcY",
|
|
},
|
|
"rewards.withdraw.amount": {
|
|
"100",
|
|
"45",
|
|
},
|
|
"rewards.withdraw.balance": {
|
|
"1500",
|
|
"999",
|
|
},
|
|
"transfer.sender": {
|
|
"AddrC",
|
|
},
|
|
"transfer.recipient": {
|
|
"AddrD",
|
|
},
|
|
"transfer.amount": {
|
|
"160",
|
|
},
|
|
}
|
|
|
|
var apiTypeEvents = []types.Event{
|
|
{
|
|
Type: "tm",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "event",
|
|
Value: "Tx",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "tm",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "hash",
|
|
Value: "XYZ",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "tm",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "height",
|
|
Value: "5",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "rewards.withdraw",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "address",
|
|
Value: "AddrA",
|
|
},
|
|
{
|
|
Key: "address",
|
|
Value: "AddrB",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "rewards.withdraw",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "source",
|
|
Value: "SrcX",
|
|
},
|
|
{
|
|
Key: "source",
|
|
Value: "SrcY",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "rewards.withdraw",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "amount",
|
|
Value: "100",
|
|
},
|
|
{
|
|
Key: "amount",
|
|
Value: "45",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "rewards.withdraw",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "balance",
|
|
Value: "1500",
|
|
},
|
|
{
|
|
Key: "balance",
|
|
Value: "999",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "transfer",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "sender",
|
|
Value: "AddrC",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "transfer",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "recipient",
|
|
Value: "AddrD",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "transfer",
|
|
Attributes: []types.EventAttribute{
|
|
{
|
|
Key: "amount",
|
|
Value: "160",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestCompiledMatches(t *testing.T) {
|
|
var (
|
|
txDate = "2017-01-01"
|
|
txTime = "2018-05-03T14:45:00Z"
|
|
)
|
|
|
|
//nolint:lll
|
|
testCases := []struct {
|
|
s string
|
|
events map[string][]string
|
|
matches bool
|
|
}{
|
|
{`tm.events.type='NewBlock'`,
|
|
newTestEvents(`tm|events.type=NewBlock`),
|
|
true},
|
|
{`tx.gas > 7`,
|
|
newTestEvents(`tx|gas=8`),
|
|
true},
|
|
{`transfer.amount > 7`,
|
|
newTestEvents(`transfer|amount=8stake`),
|
|
true},
|
|
{`transfer.amount > 7`,
|
|
newTestEvents(`transfer|amount=8.045`),
|
|
true},
|
|
{`transfer.amount > 7.043`,
|
|
newTestEvents(`transfer|amount=8.045stake`),
|
|
true},
|
|
{`transfer.amount > 8.045`,
|
|
newTestEvents(`transfer|amount=8.045stake`),
|
|
false},
|
|
{`tx.gas > 7 AND tx.gas < 9`,
|
|
newTestEvents(`tx|gas=8`),
|
|
true},
|
|
{`body.weight >= 3.5`,
|
|
newTestEvents(`body|weight=3.5`),
|
|
true},
|
|
{`account.balance < 1000.0`,
|
|
newTestEvents(`account|balance=900`),
|
|
true},
|
|
{`apples.kg <= 4`,
|
|
newTestEvents(`apples|kg=4.0`),
|
|
true},
|
|
{`body.weight >= 4.5`,
|
|
newTestEvents(`body|weight=4.5`),
|
|
true},
|
|
{`oranges.kg < 4 AND watermellons.kg > 10`,
|
|
newTestEvents(`oranges|kg=3`, `watermellons|kg=12`),
|
|
true},
|
|
{`peaches.kg < 4`,
|
|
newTestEvents(`peaches|kg=5`),
|
|
false},
|
|
{`tx.date > DATE 2017-01-01`,
|
|
newTestEvents(`tx|date=` + time.Now().Format(syntax.DateFormat)),
|
|
true},
|
|
{`tx.date = DATE 2017-01-01`,
|
|
newTestEvents(`tx|date=` + txDate),
|
|
true},
|
|
{`tx.date = DATE 2018-01-01`,
|
|
newTestEvents(`tx|date=` + txDate),
|
|
false},
|
|
{`tx.time >= TIME 2013-05-03T14:45:00Z`,
|
|
newTestEvents(`tx|time=` + time.Now().Format(syntax.TimeFormat)),
|
|
true},
|
|
{`tx.time = TIME 2013-05-03T14:45:00Z`,
|
|
newTestEvents(`tx|time=` + txTime),
|
|
false},
|
|
{`abci.owner.name CONTAINS 'Igor'`,
|
|
newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
|
|
true},
|
|
{`abci.owner.name CONTAINS 'Igor'`,
|
|
newTestEvents(`abci|owner.name=Pavel|owner.name=Ivan`),
|
|
false},
|
|
{`abci.owner.name = 'Igor'`,
|
|
newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
|
|
true},
|
|
{`abci.owner.name = 'Ivan'`,
|
|
newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
|
|
true},
|
|
{`abci.owner.name = 'Ivan' AND abci.owner.name = 'Igor'`,
|
|
newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
|
|
true},
|
|
{`abci.owner.name = 'Ivan' AND abci.owner.name = 'John'`,
|
|
newTestEvents(`abci|owner.name=Igor|owner.name=Ivan`),
|
|
false},
|
|
{`tm.events.type='NewBlock'`,
|
|
newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
|
|
true},
|
|
{`app.name = 'fuzzed'`,
|
|
newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
|
|
true},
|
|
{`tm.events.type='NewBlock' AND app.name = 'fuzzed'`,
|
|
newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
|
|
true},
|
|
{`tm.events.type='NewHeader' AND app.name = 'fuzzed'`,
|
|
newTestEvents(`tm|events.type=NewBlock`, `app|name=fuzzed`),
|
|
false},
|
|
{`slash EXISTS`,
|
|
newTestEvents(`slash|reason=missing_signature|power=6000`),
|
|
true},
|
|
{`slash EXISTS`,
|
|
newTestEvents(`transfer|recipient=cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz|sender=cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5`),
|
|
false},
|
|
{`slash.reason EXISTS AND slash.power > 1000`,
|
|
newTestEvents(`slash|reason=missing_signature|power=6000`),
|
|
true},
|
|
{`slash.reason EXISTS AND slash.power > 1000`,
|
|
newTestEvents(`slash|reason=missing_signature|power=500`),
|
|
false},
|
|
{`slash.reason EXISTS`,
|
|
newTestEvents(`transfer|recipient=cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz|sender=cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5`),
|
|
false},
|
|
|
|
// Test cases based on the OpenAPI examples.
|
|
{`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA'`,
|
|
apiEvents, true},
|
|
{`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA' AND rewards.withdraw.source = 'SrcY'`,
|
|
apiEvents, true},
|
|
{`tm.event = 'Tx' AND transfer.sender = 'AddrA'`,
|
|
apiEvents, false},
|
|
{`tm.event = 'Tx' AND transfer.sender = 'AddrC'`,
|
|
apiEvents, true},
|
|
{`tm.event = 'Tx' AND transfer.sender = 'AddrZ'`,
|
|
apiEvents, false},
|
|
{`tm.event = 'Tx' AND rewards.withdraw.address = 'AddrZ'`,
|
|
apiEvents, false},
|
|
{`tm.event = 'Tx' AND rewards.withdraw.source = 'W'`,
|
|
apiEvents, false},
|
|
}
|
|
|
|
// NOTE: The original implementation allowed arbitrary prefix matches on
|
|
// attribute tags, e.g., "sl" would match "slash".
|
|
//
|
|
// That is weird and probably wrong: "foo.ba" should not match "foo.bar",
|
|
// or there is no way to distinguish the case where there were two values
|
|
// for "foo.bar" or one value each for "foo.ba" and "foo.bar".
|
|
//
|
|
// Apart from a single test case, I could not find any attested usage of
|
|
// this implementation detail. It isn't documented in the OpenAPI docs and
|
|
// is not shown in any of the example inputs.
|
|
//
|
|
// On that basis, I removed that test case. This implementation still does
|
|
// correctly handle variable type/attribute splits ("x", "y.z" / "x.y", "z")
|
|
// since that was required by the original "flattened" event representation.
|
|
|
|
for i, tc := range testCases {
|
|
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
|
|
c, err := query.New(tc.s)
|
|
if err != nil {
|
|
t.Fatalf("NewCompiled %#q: unexpected error: %v", tc.s, err)
|
|
}
|
|
|
|
got, err := c.Matches(tc.events)
|
|
if err != nil {
|
|
t.Errorf("Query: %#q\nInput: %+v\nMatches: got error %v",
|
|
tc.s, tc.events, err)
|
|
}
|
|
if got != tc.matches {
|
|
t.Errorf("Query: %#q\nInput: %+v\nMatches: got %v, want %v",
|
|
tc.s, tc.events, got, tc.matches)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func sortEvents(events []types.Event) []types.Event {
|
|
sort.Slice(events, func(i, j int) bool {
|
|
if events[i].Type == events[j].Type {
|
|
return events[i].Attributes[0].Key < events[j].Attributes[0].Key
|
|
}
|
|
return events[i].Type < events[j].Type
|
|
})
|
|
return events
|
|
}
|
|
|
|
func TestExpandEvents(t *testing.T) {
|
|
expanded := query.ExpandEvents(apiEvents)
|
|
bz, err := json.Marshal(sortEvents(expanded))
|
|
require.NoError(t, err)
|
|
bz2, err := json.Marshal(sortEvents(apiTypeEvents))
|
|
require.NoError(t, err)
|
|
if string(bz) != string(bz2) {
|
|
t.Errorf("got %s, want %v", string(bz), string(bz2))
|
|
}
|
|
}
|
|
|
|
func TestAllMatchesAll(t *testing.T) {
|
|
events := newTestEvents(
|
|
``,
|
|
`Asher|Roth=`,
|
|
`Route|66=`,
|
|
`Rilly|Blue=`,
|
|
)
|
|
keys := make([]string, 0)
|
|
for k := range events {
|
|
keys = append(keys, k)
|
|
}
|
|
for _, key := range keys {
|
|
delete(events, key)
|
|
match, err := query.All.Matches(events)
|
|
if err != nil {
|
|
t.Errorf("Matches failed: %v", err)
|
|
} else if !match {
|
|
t.Errorf("Did not match on %+v ", events)
|
|
}
|
|
}
|
|
}
|
|
|
|
// newTestEvent constructs an Event message from a template string.
|
|
// The format is "type|attr1=val1|attr2=val2|...".
|
|
func addNewTestEvent(events map[string][]string, s string) {
|
|
parts := strings.Split(s, "|")
|
|
key := parts[0]
|
|
for _, kv := range parts[1:] {
|
|
k, v := splitKV(kv)
|
|
k = key + "." + k
|
|
events[k] = append(events[k], v)
|
|
}
|
|
}
|
|
|
|
// newTestEvents constructs a slice of Event messages by applying newTestEvent
|
|
// to each element of ss.
|
|
func newTestEvents(ss ...string) map[string][]string {
|
|
events := make(map[string][]string)
|
|
for _, s := range ss {
|
|
addNewTestEvent(events, s)
|
|
}
|
|
return events
|
|
}
|
|
|
|
func splitKV(s string) (key, value string) {
|
|
kv := strings.SplitN(s, "=", 2)
|
|
return kv[0], kv[1]
|
|
}
|