Skip to content

Commit 21c4d36

Browse files
authored
Merge pull request #224 from hyperledger-firefly/ws-only-listener
Move all RPC calls to single client that chooses WS/HTTP
2 parents 499cdbf + 5a6081b commit 21c4d36

25 files changed

Lines changed: 903 additions & 168 deletions

config.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
8383
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
8484
|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
85+
|rpcRoutingMode|Which connection JSON/RPC calls are made on when a WebSocket is enabled. 'auto' routes node-sticky chain queries (blocks, logs, filters, receipts) to the WebSocket and stateless calls (submission, gas, balance) to the HTTP connection pool. 'ws' routes everything to the WebSocket. 'http' routes everything to the HTTP connection pool, leaving the WebSocket to carry only the newHeads subscription. 'legacy' reproduces the routing used before this setting existed, where only the block listener used the WebSocket|http,ws,auto,legacy|`auto`
8586
|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s`
8687
|traceTXForRevertReason|Enable the use of transaction trace functions (e.g. debug_traceTransaction) to obtain transaction revert reasons. This can place a high load on the EVM client.|`boolean`|`false`
8788
|txCacheSize|Maximum of transactions to hold in the transaction info cache|`int`|`250`

internal/ethereum/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package ethereum
1919
import (
2020
"github.com/hyperledger-firefly/common/pkg/config"
2121
"github.com/hyperledger-firefly/common/pkg/wsclient"
22+
"github.com/hyperledger-firefly/evmconnect/pkg/ethrpc"
2223
"github.com/hyperledger-firefly/transaction-manager/pkg/ffcapi"
2324
)
2425

@@ -51,6 +52,7 @@ const (
5152
HederaCompatibilityMode = "hederaCompatibilityMode"
5253
TraceTXForRevertReason = "traceTXForRevertReason"
5354
WebSocketsEnabled = "ws.enabled"
55+
RPCRoutingMode = "rpcRoutingMode"
5456
MaxAsyncBlockFetchConcurrency = "maxAsyncBlockFetchConcurrency"
5557
UseGetBlockReceipts = "useGetBlockReceipts"
5658
)
@@ -72,6 +74,7 @@ const (
7274
func InitConfig(conf config.Section) {
7375
wsclient.InitConfig(conf)
7476
conf.AddKnownKey(WebSocketsEnabled, false)
77+
conf.AddKnownKey(RPCRoutingMode, string(ethrpc.RoutingModeAuto))
7578
conf.AddKnownKey(BlockCacheSize, 250)
7679
conf.AddKnownKey(ReceiptCacheEnabled, false)
7780
conf.AddKnownKey(ReceiptCacheSize, 5000)

internal/ethereum/estimate_gas.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (c *ethConnector) gasEstimate(ctx context.Context, tx *ethsigner.Transactio
7070

7171
// Do the gas estimation
7272
var gasEstimate ethtypes.HexInteger
73-
rpcErr := c.backend.CallRPC(ctx, &gasEstimate, "eth_estimateGas", tx)
73+
rpcErr := c.rpc.CallRPC(ctx, &gasEstimate, "eth_estimateGas", tx)
7474
if rpcErr != nil {
7575
if reason, revertErr := c.attemptProcessingRevertData(ctx, errors, rpcErr); revertErr != nil {
7676
return nil, reason, revertErr

internal/ethereum/ethereum.go

Lines changed: 45 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,11 @@ import (
3838
"github.com/hyperledger-firefly/evmconnect/pkg/ethrpc"
3939
"github.com/hyperledger-firefly/signer/pkg/abi"
4040
"github.com/hyperledger-firefly/signer/pkg/ethtypes"
41-
"github.com/hyperledger-firefly/signer/pkg/rpcbackend"
4241
"github.com/hyperledger-firefly/transaction-manager/pkg/ffcapi"
4342
)
4443

4544
type ethConnector struct {
46-
backend rpcbackend.Backend
47-
wsBackend rpcbackend.WebSocketRPCClient
45+
rpc ethrpc.Client
4846
chainTrackingMode ffcapi.ChainTrackingMode
4947

5048
serializer *abi.Serializer
@@ -68,18 +66,48 @@ type ethConnector struct {
6866
type Connector interface {
6967
ffcapi.API
7068

71-
// RPC returns the http JSON/RPC client
72-
RPC() rpcbackend.RPC
73-
74-
// WSRPC returns the websocket JSON/RPC client
75-
// NOTE: websocket client will be nil if websockets are not enabled
76-
WSRPC() rpcbackend.WebSocketRPCClient
69+
// RPC returns the JSON/RPC client used for everything the connector does. It owns both
70+
// the HTTP connection pool and the WebSocket (when enabled), and routes each call to one
71+
// of them based on the method name and the configured routing mode.
72+
RPC() ethrpc.Client
7773

7874
// Get the high level block listener functionality, which provides a view of the head of the chain
7975
BlockListener() ethblocklistener.BlockListener
8076
}
8177

78+
// NewEthereumConnector builds the JSON/RPC client from configuration, and the connector over it.
8279
func NewEthereumConnector(ctx context.Context, conf config.Section) (cc Connector, err error) {
80+
if conf.GetString(ffresty.HTTPConfigURL) == "" {
81+
return nil, i18n.NewError(ctx, msgs.MsgMissingBackendURL)
82+
}
83+
84+
var wsConf *wsclient.WSConfig
85+
if conf.GetBool(WebSocketsEnabled) {
86+
if wsConf, err = wsclient.GenerateConfig(ctx, conf); err != nil {
87+
return nil, err
88+
}
89+
}
90+
httpConf, err := ffresty.GenerateConfig(ctx, conf)
91+
if err != nil {
92+
return nil, err
93+
}
94+
95+
rpc, err := ethrpc.NewClient(ctx, &ethrpc.Config{
96+
RoutingMode: ethrpc.RoutingMode(conf.GetString(RPCRoutingMode)),
97+
HTTP: httpConf,
98+
WS: wsConf,
99+
MaxConcurrentRequests: conf.GetInt64(MaxConcurrentRequests),
100+
})
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
return NewEthereumConnectorWithRPC(ctx, conf, rpc)
106+
}
107+
108+
// NewEthereumConnectorWithRPC builds the connector over a pre-constructed JSON/RPC client.
109+
// The caller owns closing the client - see ethConnector.WaitClosed.
110+
func NewEthereumConnectorWithRPC(ctx context.Context, conf config.Section, rpc ethrpc.Client) (cc Connector, err error) {
83111

84112
chainTrackingMode := ffcapi.ChainTrackingMode(conf.GetString(ChainTrackingMode))
85113
if chainTrackingMode == "" {
@@ -90,6 +118,7 @@ func NewEthereumConnector(ctx context.Context, conf config.Section) (cc Connecto
90118
}
91119

92120
c := &ethConnector{
121+
rpc: rpc,
93122
eventStreams: make(map[fftypes.UUID]*eventStream),
94123
catchupPageSize: conf.GetInt64(EventsCatchupPageSize),
95124
catchupThreshold: conf.GetInt64(EventsCatchupThreshold),
@@ -115,41 +144,13 @@ func NewEthereumConnector(ctx context.Context, conf config.Section) (cc Connecto
115144
return nil, i18n.WrapError(ctx, err, msgs.MsgCacheInitFail, "transaction")
116145
}
117146

118-
if conf.GetString(ffresty.HTTPConfigURL) == "" {
119-
return nil, i18n.NewError(ctx, msgs.MsgMissingBackendURL)
120-
}
121147
c.gasEstimationFactor = big.NewFloat(conf.GetFloat64(ConfigGasEstimationFactor))
122148

123149
c.catchupDownscaleRegex, err = regexp.Compile(conf.GetString(EventsCatchupDownscaleRegex))
124150
if err != nil {
125151
return nil, i18n.WrapError(ctx, err, msgs.MsgInvalidRegex, c.catchupDownscaleRegex)
126152
}
127153

128-
var wsConf *wsclient.WSConfig
129-
var httpConf *ffresty.Config
130-
if conf.GetBool(WebSocketsEnabled) {
131-
// If websockets are enabled, then they are used selectively (block listening/query)
132-
// not as a full replacement for HTTP.
133-
wsConf, err = wsclient.GenerateConfig(ctx, conf)
134-
}
135-
136-
if err == nil {
137-
httpConf, err = ffresty.GenerateConfig(ctx, conf)
138-
}
139-
140-
if err != nil {
141-
return nil, err
142-
}
143-
144-
httpClient := ffresty.NewWithConfig(ctx, *httpConf)
145-
c.backend = rpcbackend.NewRPCClientWithOption(httpClient, rpcbackend.RPCClientOptions{
146-
MaxConcurrentRequest: conf.GetInt64(MaxConcurrentRequests),
147-
})
148-
149-
if wsConf != nil {
150-
c.wsBackend = rpcbackend.NewWSRPCClient(wsConf)
151-
}
152-
153154
c.serializer = abi.NewSerializer().SetByteSerializer(abi.HexByteSerializer0xPrefix)
154155
switch conf.Get(ConfigDataFormat) {
155156
case "map":
@@ -169,7 +170,7 @@ func NewEthereumConnector(ctx context.Context, conf config.Section) (cc Connecto
169170
return name
170171
})
171172

172-
if c.blockListener, err = ethblocklistener.NewBlockListenerSupplyBackend(ctx, c.retry.Retry, &ethblocklistener.BlockListenerConfig{
173+
if c.blockListener, err = ethblocklistener.NewBlockListener(ctx, c.retry.Retry, &ethblocklistener.BlockListenerConfig{
173174
BlockPollingInterval: conf.GetDuration(BlockPollingInterval),
174175
MonitoredHeadLength: int(c.checkpointBlockGap),
175176
HederaCompatibilityMode: conf.GetBool(HederaCompatibilityMode),
@@ -179,19 +180,15 @@ func NewEthereumConnector(ctx context.Context, conf config.Section) (cc Connecto
179180
MaxAsyncBlockFetchConcurrency: conf.GetInt(MaxAsyncBlockFetchConcurrency),
180181
UseGetBlockReceipts: conf.GetBool(UseGetBlockReceipts),
181182
ChainTrackingMode: c.chainTrackingMode,
182-
}, c.backend, c.wsBackend); err != nil {
183+
}, c.rpc); err != nil {
183184
return nil, err
184185
}
185186

186187
return c, nil
187188
}
188189

189-
func (c *ethConnector) RPC() rpcbackend.RPC {
190-
return c.backend
191-
}
192-
193-
func (c *ethConnector) WSRPC() rpcbackend.WebSocketRPCClient {
194-
return c.wsBackend
190+
func (c *ethConnector) RPC() ethrpc.Client {
191+
return c.rpc
195192
}
196193

197194
func (c *ethConnector) BlockListener() ethblocklistener.BlockListener {
@@ -206,6 +203,9 @@ func (c *ethConnector) WaitClosed() {
206203
for _, s := range c.eventStreams {
207204
<-s.streamLoopDone
208205
}
206+
if c.rpc != nil {
207+
c.rpc.Close()
208+
}
209209
}
210210

211211
func withDeprecatedConfFallback[T any](conf config.Section, getter func(string) T, deprecatedKey, newKey string) T {

internal/ethereum/ethereum_test.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/hyperledger-firefly/evmconnect/pkg/ethblocklistener"
3232
"github.com/hyperledger-firefly/evmconnect/pkg/ethrpc"
3333
"github.com/hyperledger-firefly/signer/pkg/abi"
34+
"github.com/hyperledger-firefly/signer/pkg/rpcbackend"
3435
"github.com/hyperledger-firefly/transaction-manager/pkg/ffcapi"
3536
"github.com/sirupsen/logrus"
3637
"github.com/stretchr/testify/assert"
@@ -40,6 +41,12 @@ import (
4041

4142
func strPtr(s string) *string { return &s }
4243

44+
func utRPC(t *testing.T, mRPC rpcbackend.RPC) ethrpc.Client {
45+
rpc, err := ethrpc.NewClientWithBackends(t.Context(), ethrpc.RoutingModeHTTP, mRPC, nil)
46+
require.NoError(t, err)
47+
return rpc
48+
}
49+
4350
func newTestConnector(t *testing.T, confSetup ...func(conf config.Section)) (context.Context, *ethConnector, *rpcbackendmocks.Backend, func()) {
4451
ctx, c, mRPC, done := newTestConnectorWithNoBlockerFilterDefaultMocks(t, confSetup...)
4552

@@ -64,13 +71,13 @@ func newTestConnectorWithNoBlockerFilterDefaultMocks(t *testing.T, confSetup ...
6471
fn(conf)
6572
}
6673
ctx, done := context.WithCancel(context.Background())
67-
cc, err := NewEthereumConnector(ctx, conf)
74+
rpc, err := ethrpc.NewClientWithBackends(ctx, ethrpc.RoutingModeHTTP, mRPC, nil)
75+
require.NoError(t, err)
76+
cc, err := NewEthereumConnectorWithRPC(ctx, conf, rpc)
6877
assert.NoError(t, err)
6978
assert.NotNil(t, cc.RPC())
7079

7180
c := cc.(*ethConnector)
72-
c.backend = mRPC
73-
cc.BlockListener().UTSetBackend(mRPC)
7481
return ctx, c, mRPC, func() {
7582
done()
7683
mRPC.AssertExpectations(t)
@@ -87,12 +94,17 @@ func TestConnectorInit(t *testing.T) {
8794
cc, err := NewEthereumConnector(context.Background(), conf)
8895
assert.Regexp(t, "FF23025", err)
8996

97+
conf.Set(ffresty.HTTPConfigURL, "http://localhost:8545")
9098
conf.Set(ChainTrackingMode, "wrong")
9199
_, err = NewEthereumConnector(context.Background(), conf)
92100
assert.Regexp(t, "FF23069.*wrong", err)
93101

102+
conf.Set(RPCRoutingMode, "wrong")
103+
_, err = NewEthereumConnector(context.Background(), conf)
104+
assert.Regexp(t, "FF23075.*wrong", err)
105+
106+
conf.Set(RPCRoutingMode, ethrpc.RoutingModeAuto)
94107
conf.Set(ChainTrackingMode, "")
95-
conf.Set(ffresty.HTTPConfigURL, "http://localhost:8545")
96108
conf.Set(WebSocketsEnabled, true)
97109
conf.Set(EventsCatchupThreshold, 1)
98110
conf.Set(EventsCatchupPageSize, 500)
@@ -237,7 +249,7 @@ func TestRetryDefaultsFor429(t *testing.T) {
237249
cc, err := NewEthereumConnector(ctx, conf)
238250
assert.NoError(t, err)
239251
assert.NotNil(t, cc.RPC())
240-
assert.Nil(t, cc.WSRPC())
252+
assert.False(t, cc.RPC().HasWebSocket())
241253
defer done()
242254

243255
// Start a simple HTTP server that always replies with 429 Too Many Requests

internal/ethereum/event_listener.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,8 @@ func (l *listener) listenerCatchupLoop() {
240240
}
241241
} else {
242242
log.L(ctx).Errorf("Failed to query block range fromBlock=%d toBlock=%d: %s", fromBlock, toBlock, err)
243-
failCount++
244243
}
244+
failCount++ // for exponential backoff calculation
245245
continue
246246
}
247247
log.L(ctx).Infof("Listener catchup fromBlock=%d toBlock=%d events=%d", fromBlock, toBlock, len(events))

internal/ethereum/event_stream.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ func (es *eventStream) leadGroupCatchup() bool {
304304
func (es *eventStream) uninstallFilter(filter *string) {
305305
if *filter != "" {
306306
var res bool
307-
if err := es.c.backend.CallRPC(es.ctx, &res, "eth_uninstallFilter", filter); err != nil {
307+
if err := es.c.rpc.CallRPC(es.ctx, &res, "eth_uninstallFilter", filter); err != nil {
308308
log.L(es.ctx).Warnf("Error uninstalling filter '%v': %s", filter, err.Message)
309309
} else {
310310
log.L(es.ctx).Debugf("Uninstalled filter '%v': %t", filter, res)
@@ -368,7 +368,7 @@ func (es *eventStream) leadGroupSteadyState() bool {
368368
}
369369

370370
// Create the new filter
371-
err := es.c.backend.CallRPC(es.ctx, &filter, "eth_newFilter", &ethrpc.LogFilterJSONRPC{
371+
err := es.c.rpc.CallRPC(es.ctx, &filter, "eth_newFilter", &ethrpc.LogFilterJSONRPC{
372372
FromBlock: ethtypes.NewHexInteger64(fromBlock),
373373
Topics: [][]ethtypes.HexBytes0xPrefix{
374374
ag.signatureSet,
@@ -384,7 +384,7 @@ func (es *eventStream) leadGroupSteadyState() bool {
384384
}
385385
// Get the next batch of logs
386386
var ethLogs []*ethrpc.LogJSONRPC
387-
rpcErr := es.c.backend.CallRPC(es.ctx, &ethLogs, filterRPCMethodToUse, filter)
387+
rpcErr := es.c.rpc.CallRPC(es.ctx, &ethLogs, filterRPCMethodToUse, filter)
388388
// If we fail to query we just retry - setting filter to nil if not found
389389
if rpcErr != nil {
390390
if etherrors.MapError(etherrors.FilterRPCMethods, rpcErr.Error()) == ffcapi.ErrorReasonNotFound {
@@ -596,7 +596,7 @@ func (es *eventStream) getBlockRangeEvents(ctx context.Context, ag *aggregatedLi
596596
logFilterJSONRPCReq.Address = []*ethtypes.Address0xHex{ag.listeners[0].config.filters[0].Address}
597597
}
598598

599-
rpcErr := es.c.backend.CallRPC(ctx, &ethLogs, "eth_getLogs", logFilterJSONRPCReq)
599+
rpcErr := es.c.rpc.CallRPC(ctx, &ethLogs, "eth_getLogs", logFilterJSONRPCReq)
600600
if rpcErr != nil {
601601
return nil, rpcErr.Error()
602602
}

internal/ethereum/event_stream_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -996,7 +996,7 @@ func TestStreamCleanupFilterOK(t *testing.T) {
996996
es := &eventStream{
997997
ctx: context.Background(),
998998
c: &ethConnector{
999-
backend: mRPC,
999+
rpc: utRPC(t, mRPC),
10001000
},
10011001
}
10021002

@@ -1015,7 +1015,7 @@ func TestStreamCleanupFilterFailLog(t *testing.T) {
10151015
es := &eventStream{
10161016
ctx: context.Background(),
10171017
c: &ethConnector{
1018-
backend: mRPC,
1018+
rpc: utRPC(t, mRPC),
10191019
},
10201020
}
10211021

internal/ethereum/exec_query.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func (c *ethConnector) callTransaction(ctx context.Context, tx *ethsigner.Transa
9191
if blockNumber != nil {
9292
blockNumberStr = *blockNumber
9393
}
94-
rpcErr := c.backend.CallRPC(ctx, &outputData, "eth_call", tx, blockNumberStr)
94+
rpcErr := c.rpc.CallRPC(ctx, &outputData, "eth_call", tx, blockNumberStr)
9595
if rpcErr != nil {
9696
if reason, revertErr := c.attemptProcessingRevertData(ctx, errors, rpcErr); revertErr != nil {
9797
return nil, reason, revertErr

internal/ethereum/get_address_balance.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func (c *ethConnector) AddressBalance(ctx context.Context, req *ffcapi.AddressBa
3131
if blockTag == "" {
3232
blockTag = "latest"
3333
}
34-
rpcErr := c.backend.CallRPC(ctx, &addressBalance, "eth_getBalance", req.Address, blockTag)
34+
rpcErr := c.rpc.CallRPC(ctx, &addressBalance, "eth_getBalance", req.Address, blockTag)
3535
if rpcErr != nil {
3636
return nil, "", rpcErr.Error()
3737
}

0 commit comments

Comments
 (0)