Skip to content

Commit

Permalink
fix: bug in evm-indexer (#77)
Browse files Browse the repository at this point in the history
* fix: fix bug in evm-indexer

* fix: change documentation link

* docs: improve names for tests

* chore: update cenkalti/backoff library

* refactor: use exponential-back-off instead of constant-back-off

* refactor: use constants for back-off configuration

* fix: linter

* chore: increase status client timeout
  • Loading branch information
evgeniy-scherbina committed Oct 2, 2024
1 parent 65384e0 commit 24f2466
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ issues:
- lll
- path: rpc/websockets.go
text: 'G114: Use of net/http serve function that has no support for setting timeouts'
exclude:
- "G115: integer overflow conversion"
max-same-issues: 50

linters-settings:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/armon/go-metrics v0.4.1
github.com/btcsuite/btcd v0.23.4
github.com/btcsuite/btcd/btcutil v1.1.3
github.com/cenkalti/backoff/v4 v4.3.0
github.com/cometbft/cometbft v0.37.4
github.com/cometbft/cometbft-db v0.9.1
github.com/cosmos/cosmos-proto v1.0.0-beta.5
Expand Down Expand Up @@ -67,7 +68,6 @@ require (
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,8 @@ github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
Expand Down
4 changes: 2 additions & 2 deletions gomod2nix.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ schema = 3
version = "v1.0.1"
hash = "sha256-vix0j/KGNvoKjhlKgVeSLY6un2FHeIEoZWMC4z3yvZ4="
[mod."github.com/cenkalti/backoff/v4"]
version = "v4.1.3"
hash = "sha256-u6MEDopHoTWAZoVvvXOKnAg++xre53YgQx0gmf6t2KU="
version = "v4.3.0"
hash = "sha256-wfVjNZsGG1WoNC5aL+kdcy6QXPgZo4THAevZ1787md8="
[mod."github.com/cespare/xxhash"]
version = "v1.1.0"
hash = "sha256-nVDTtXH9PC3yJ0THaQZEN243UP9xgLi/clt5xRqj3+M="
Expand Down
37 changes: 37 additions & 0 deletions server/indexer_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ package server

import (
"context"
"errors"
"time"

"github.com/cenkalti/backoff/v4"
"github.com/cometbft/cometbft/libs/service"
rpcclient "github.com/cometbft/cometbft/rpc/client"
"github.com/cometbft/cometbft/types"
Expand All @@ -30,6 +32,9 @@ const (
ServiceName = "EVMIndexerService"

NewBlockWaitTimeout = 60 * time.Second

statusClientMaxRetryInterval = time.Second * 10
statusClientTimeout = time.Hour * 48
)

// EVMIndexerService indexes transactions for json-rpc service.
Expand All @@ -54,6 +59,17 @@ func NewEVMIndexerService(
// and indexing them by events.
func (eis *EVMIndexerService) OnStart() error {
ctx := context.Background()

// when kava in state-sync mode, it returns zero as latest_block_height, which leads to undesired behavior, more
// details here: https://github.com/Kava-Labs/ethermint/issues/79 to prevent this we wait until state-sync will finish
exponentialBackOff := backoff.NewExponentialBackOff(
backoff.WithMaxInterval(statusClientMaxRetryInterval), // set max retry interval
backoff.WithMaxElapsedTime(statusClientTimeout), // set timeout
)
if err := waitUntilClientReady(ctx, eis.client, exponentialBackOff); err != nil {
return err
}

status, err := eis.client.Status(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -122,3 +138,24 @@ func (eis *EVMIndexerService) OnStart() error {
}
}
}

// waitUntilClientReady waits until StatusClient is ready to serve requests
func waitUntilClientReady(ctx context.Context, client rpcclient.StatusClient, b backoff.BackOff) error {
err := backoff.Retry(func() error {
status, err := client.Status(ctx)
if err != nil {
return err
}

if status.SyncInfo.LatestBlockHeight == 0 {
return errors.New("node isn't ready, possibly in state sync process")
}

return nil
}, b)
if err != nil {
return err
}

return nil
}
93 changes: 93 additions & 0 deletions server/indexer_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package server

import (
"context"
"math"
"testing"
"time"

"github.com/cenkalti/backoff/v4"
"github.com/stretchr/testify/require"

coretypes "github.com/cometbft/cometbft/rpc/core/types"
)

var (
failedResponse = &coretypes.ResultStatus{
SyncInfo: coretypes.SyncInfo{
LatestBlockHeight: 0,
},
}

successfulResponse = &coretypes.ResultStatus{
SyncInfo: coretypes.SyncInfo{
LatestBlockHeight: 1,
},
}
)

type statusClientMock struct {
// retries left before success response
retriesLeft uint
}

func newStatusClientMock(retriesLeft uint) *statusClientMock {
return &statusClientMock{
retriesLeft: retriesLeft,
}
}

func (m *statusClientMock) Status(context.Context) (*coretypes.ResultStatus, error) {
if m.retriesLeft == 0 {
return successfulResponse, nil
}

m.retriesLeft--
return failedResponse, nil
}

func TestWaitUntilClientReady(t *testing.T) {
for _, tc := range []struct {
desc string
retriesLeft uint
}{
{
desc: "return successful response right away",
retriesLeft: 0,
},
{
desc: "return successful response after one retry",
retriesLeft: 1,
},
{
desc: "return successful response after 10 retries",
retriesLeft: 10,
},
} {
t.Run(tc.desc, func(t *testing.T) {
ctxb := context.Background()
mock := newStatusClientMock(tc.retriesLeft)

err := waitUntilClientReady(ctxb, mock, backoff.NewConstantBackOff(time.Nanosecond))
require.NoError(t, err)
require.Equal(t, uint(0), mock.retriesLeft)
})
}
}

func TestWaitUntilClientReadyTimeout(t *testing.T) {
ctxb := context.Background()
// create a mock client which always returns an error
mock := newStatusClientMock(math.MaxUint)

exponentialBackOff := backoff.NewExponentialBackOff(
backoff.WithInitialInterval(time.Millisecond),
backoff.WithMaxInterval(time.Millisecond*10),
backoff.WithMaxElapsedTime(time.Millisecond*100),
)

err := waitUntilClientReady(ctxb, mock, exponentialBackOff)
// make sure error is propagated in case of timeout
require.Error(t, err)
require.Contains(t, err.Error(), "node isn't ready, possibly in state sync process")
}
2 changes: 1 addition & 1 deletion tests/rpc/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func CallWithError(method string, params interface{}) (*Response, error) {
}

if rpcRes.Error != nil {
return nil, fmt.Errorf(rpcRes.Error.Message)
return nil, errors.New(rpcRes.Error.Message)
}

return rpcRes, nil
Expand Down

0 comments on commit 24f2466

Please sign in to comment.