From bdb7eed18b32e3bed6fa2f50ad194f7c9b2588d6 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 6 Sep 2024 16:31:55 -0500 Subject: [PATCH 01/27] add functions for newHead polling --- common/client/mock_node_client_test.go | 67 +++++++++++ common/client/mock_rpc_test.go | 123 ++++++++++++++++++++ common/client/multi_node.go | 9 ++ common/client/types.go | 2 + core/chains/evm/client/mocks/rpc_client.go | 125 +++++++++++++++++++++ core/chains/evm/client/rpc_client.go | 31 +++++ 6 files changed, 357 insertions(+) diff --git a/common/client/mock_node_client_test.go b/common/client/mock_node_client_test.go index 5643dcde90e..ab1aec77c05 100644 --- a/common/client/mock_node_client_test.go +++ b/common/client/mock_node_client_test.go @@ -626,6 +626,73 @@ func (_c *mockNodeClient_SubscribeToHeads_Call[CHAIN_ID, HEAD]) RunAndReturn(run return _c } +// SubscribeToNewHeads provides a mock function with given fields: _a0 +func (_m *mockNodeClient[CHAIN_ID, HEAD]) SubscribeToNewHeads(_a0 context.Context) (<-chan HEAD, types.Subscription, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for SubscribeToNewHeads") + } + + var r0 <-chan HEAD + var r1 types.Subscription + var r2 error + if rf, ok := ret.Get(0).(func(context.Context) (<-chan HEAD, types.Subscription, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) <-chan HEAD); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan HEAD) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) types.Subscription); ok { + r1 = rf(_a0) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(types.Subscription) + } + } + + if rf, ok := ret.Get(2).(func(context.Context) error); ok { + r2 = rf(_a0) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// mockNodeClient_SubscribeToNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToNewHeads' +type mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID types.ID, HEAD Head] struct { + *mock.Call +} + +// SubscribeToNewHeads is a helper method to define mock.On call +// - _a0 context.Context +func (_e *mockNodeClient_Expecter[CHAIN_ID, HEAD]) SubscribeToNewHeads(_a0 interface{}) *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD] { + return &mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD]{Call: _e.mock.On("SubscribeToNewHeads", _a0)} +} + +func (_c *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD]) Run(run func(_a0 context.Context)) *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD] { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD]) Return(_a0 <-chan HEAD, _a1 types.Subscription, _a2 error) *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD] { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD]) RunAndReturn(run func(context.Context) (<-chan HEAD, types.Subscription, error)) *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD] { + _c.Call.Return(run) + return _c +} + // SubscribersCount provides a mock function with given fields: func (_m *mockNodeClient[CHAIN_ID, HEAD]) SubscribersCount() int32 { ret := _m.Called() diff --git a/common/client/mock_rpc_test.go b/common/client/mock_rpc_test.go index 00473c66369..418e825b2d7 100644 --- a/common/client/mock_rpc_test.go +++ b/common/client/mock_rpc_test.go @@ -983,6 +983,62 @@ func (_c *mockRPC_LINKBalance_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, return _c } +// LatestBlock provides a mock function with given fields: _a0 +func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) LatestBlock(_a0 context.Context) (HEAD, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for LatestBlock") + } + + var r0 HEAD + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (HEAD, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) HEAD); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(HEAD) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockRPC_LatestBlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestBlock' +type mockRPC_LatestBlock_Call[CHAIN_ID types.ID, SEQ types.Sequence, ADDR types.Hashable, BLOCK_HASH types.Hashable, TX interface{}, TX_HASH types.Hashable, EVENT interface{}, EVENT_OPS interface{}, TX_RECEIPT types.Receipt[TX_HASH, BLOCK_HASH], FEE feetypes.Fee, HEAD types.Head[BLOCK_HASH], BATCH_ELEM interface{}] struct { + *mock.Call +} + +// LatestBlock is a helper method to define mock.On call +// - _a0 context.Context +func (_e *mockRPC_Expecter[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) LatestBlock(_a0 interface{}) *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { + return &mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]{Call: _e.mock.On("LatestBlock", _a0)} +} + +func (_c *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Run(run func(_a0 context.Context)) *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Return(_a0 HEAD, _a1 error) *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) RunAndReturn(run func(context.Context) (HEAD, error)) *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { + _c.Call.Return(run) + return _c +} + // LatestBlockHeight provides a mock function with given fields: _a0 func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) LatestBlockHeight(_a0 context.Context) (*big.Int, error) { ret := _m.Called(_a0) @@ -1652,6 +1708,73 @@ func (_c *mockRPC_SubscribeToHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_ return _c } +// SubscribeToNewHeads provides a mock function with given fields: _a0 +func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) SubscribeToNewHeads(_a0 context.Context) (<-chan HEAD, types.Subscription, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for SubscribeToNewHeads") + } + + var r0 <-chan HEAD + var r1 types.Subscription + var r2 error + if rf, ok := ret.Get(0).(func(context.Context) (<-chan HEAD, types.Subscription, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) <-chan HEAD); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan HEAD) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) types.Subscription); ok { + r1 = rf(_a0) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(types.Subscription) + } + } + + if rf, ok := ret.Get(2).(func(context.Context) error); ok { + r2 = rf(_a0) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// mockRPC_SubscribeToNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToNewHeads' +type mockRPC_SubscribeToNewHeads_Call[CHAIN_ID types.ID, SEQ types.Sequence, ADDR types.Hashable, BLOCK_HASH types.Hashable, TX interface{}, TX_HASH types.Hashable, EVENT interface{}, EVENT_OPS interface{}, TX_RECEIPT types.Receipt[TX_HASH, BLOCK_HASH], FEE feetypes.Fee, HEAD types.Head[BLOCK_HASH], BATCH_ELEM interface{}] struct { + *mock.Call +} + +// SubscribeToNewHeads is a helper method to define mock.On call +// - _a0 context.Context +func (_e *mockRPC_Expecter[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) SubscribeToNewHeads(_a0 interface{}) *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { + return &mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]{Call: _e.mock.On("SubscribeToNewHeads", _a0)} +} + +func (_c *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Run(run func(_a0 context.Context)) *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Return(_a0 <-chan HEAD, _a1 types.Subscription, _a2 error) *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) RunAndReturn(run func(context.Context) (<-chan HEAD, types.Subscription, error)) *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { + _c.Call.Return(run) + return _c +} + // SubscribersCount provides a mock function with given fields: func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) SubscribersCount() int32 { ret := _m.Called() diff --git a/common/client/multi_node.go b/common/client/multi_node.go index c9250a1d620..2a5891e3726 100644 --- a/common/client/multi_node.go +++ b/common/client/multi_node.go @@ -862,3 +862,12 @@ func (c *multiNode[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OP return n.RPC().LatestFinalizedBlock(ctx) } + +func (c *multiNode[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, RPC_CLIENT, BATCH_ELEM]) LatestBlock(ctx context.Context) (head HEAD, err error) { + n, err := c.selectNode() + if err != nil { + return head, err + } + + return n.RPC().LatestBlock(ctx) +} diff --git a/common/client/types.go b/common/client/types.go index c9b6a3580eb..2347361ba1a 100644 --- a/common/client/types.go +++ b/common/client/types.go @@ -80,6 +80,7 @@ type NodeClient[ // Ensure implementation does not have a race condition when values are reset before request completion and as // a result latest ChainInfo contains information from the previous cycle. GetInterceptedChainInfo() (latest, highestUserObservations ChainInfo) + SubscribeToNewHeads(_ context.Context) (<-chan HEAD, types.Subscription, error) } // clientAPI includes all the direct RPC methods required by the generalized common client to implement its own. @@ -126,6 +127,7 @@ type clientAPI[ BlockByHash(ctx context.Context, hash BLOCK_HASH) (HEAD, error) LatestBlockHeight(context.Context) (*big.Int, error) LatestFinalizedBlock(ctx context.Context) (HEAD, error) + LatestBlock(context.Context) (HEAD, error) // Events FilterEvents(ctx context.Context, query EVENT_OPS) ([]EVENT, error) diff --git a/core/chains/evm/client/mocks/rpc_client.go b/core/chains/evm/client/mocks/rpc_client.go index 5567b3f8978..0c7ff4a9f81 100644 --- a/core/chains/evm/client/mocks/rpc_client.go +++ b/core/chains/evm/client/mocks/rpc_client.go @@ -1297,6 +1297,64 @@ func (_c *RPCClient_LINKBalance_Call) RunAndReturn(run func(context.Context, com return _c } +// LatestBlock provides a mock function with given fields: _a0 +func (_m *RPCClient) LatestBlock(_a0 context.Context) (*types.Head, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for LatestBlock") + } + + var r0 *types.Head + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*types.Head, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) *types.Head); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Head) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RPCClient_LatestBlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestBlock' +type RPCClient_LatestBlock_Call struct { + *mock.Call +} + +// LatestBlock is a helper method to define mock.On call +// - _a0 context.Context +func (_e *RPCClient_Expecter) LatestBlock(_a0 interface{}) *RPCClient_LatestBlock_Call { + return &RPCClient_LatestBlock_Call{Call: _e.mock.On("LatestBlock", _a0)} +} + +func (_c *RPCClient_LatestBlock_Call) Run(run func(_a0 context.Context)) *RPCClient_LatestBlock_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *RPCClient_LatestBlock_Call) Return(_a0 *types.Head, _a1 error) *RPCClient_LatestBlock_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RPCClient_LatestBlock_Call) RunAndReturn(run func(context.Context) (*types.Head, error)) *RPCClient_LatestBlock_Call { + _c.Call.Return(run) + return _c +} + // LatestBlockHeight provides a mock function with given fields: _a0 func (_m *RPCClient) LatestBlockHeight(_a0 context.Context) (*big.Int, error) { ret := _m.Called(_a0) @@ -2087,6 +2145,73 @@ func (_c *RPCClient_SubscribeToHeads_Call) RunAndReturn(run func(context.Context return _c } +// SubscribeToNewHeads provides a mock function with given fields: _a0 +func (_m *RPCClient) SubscribeToNewHeads(_a0 context.Context) (<-chan *types.Head, commontypes.Subscription, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for SubscribeToNewHeads") + } + + var r0 <-chan *types.Head + var r1 commontypes.Subscription + var r2 error + if rf, ok := ret.Get(0).(func(context.Context) (<-chan *types.Head, commontypes.Subscription, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) <-chan *types.Head); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan *types.Head) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) commontypes.Subscription); ok { + r1 = rf(_a0) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(commontypes.Subscription) + } + } + + if rf, ok := ret.Get(2).(func(context.Context) error); ok { + r2 = rf(_a0) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// RPCClient_SubscribeToNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToNewHeads' +type RPCClient_SubscribeToNewHeads_Call struct { + *mock.Call +} + +// SubscribeToNewHeads is a helper method to define mock.On call +// - _a0 context.Context +func (_e *RPCClient_Expecter) SubscribeToNewHeads(_a0 interface{}) *RPCClient_SubscribeToNewHeads_Call { + return &RPCClient_SubscribeToNewHeads_Call{Call: _e.mock.On("SubscribeToNewHeads", _a0)} +} + +func (_c *RPCClient_SubscribeToNewHeads_Call) Run(run func(_a0 context.Context)) *RPCClient_SubscribeToNewHeads_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *RPCClient_SubscribeToNewHeads_Call) Return(_a0 <-chan *types.Head, _a1 commontypes.Subscription, _a2 error) *RPCClient_SubscribeToNewHeads_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *RPCClient_SubscribeToNewHeads_Call) RunAndReturn(run func(context.Context) (<-chan *types.Head, commontypes.Subscription, error)) *RPCClient_SubscribeToNewHeads_Call { + _c.Call.Return(run) + return _c +} + // SubscribersCount provides a mock function with given fields: func (_m *RPCClient) SubscribersCount() int32 { ret := _m.Called() diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 0752def9949..207a55d9362 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -563,6 +563,15 @@ func (r *rpcClient) SubscribeToFinalizedHeads(ctx context.Context) (<-chan *evmt return channel, &poller, nil } +func (r *rpcClient) SubscribeToNewHeads(ctx context.Context) (<-chan *evmtypes.Head, commontypes.Subscription, error) { + // TODO to replace the interval and timeout values + poller, channel := commonclient.NewPoller[*evmtypes.Head](time.Second, r.LatestFinalizedBlock, time.Second, r.rpcLog) + if err := poller.Start(ctx); err != nil { + return nil, nil, err + } + return channel, &poller, nil +} + // GethClient wrappers func (r *rpcClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (receipt *evmtypes.Receipt, err error) { @@ -695,6 +704,28 @@ func (r *rpcClient) LatestFinalizedBlock(ctx context.Context) (head *evmtypes.He return } +func (r *rpcClient) LatestBlock(ctx context.Context) (head *evmtypes.Head, err error) { + // capture chStopInFlight to ensure we are not updating chainInfo with observations related to previous life cycle + ctx, cancel, chStopInFlight, _, _ := r.acquireQueryCtx(ctx, r.rpcTimeout) + defer cancel() + + // TODO any special treatment for some chains ? + err = r.ethGetBlockByNumber(ctx, rpc.LatestBlockNumber.String(), &head) + if err != nil { + return + } + + if head == nil { + err = r.wrapRPCClientError(ethereum.NotFound) + return + } + + head.EVMChainID = ubig.New(r.chainID) + + r.onNewHead(ctx, chStopInFlight, head) + return +} + func (r *rpcClient) astarLatestFinalizedBlock(ctx context.Context, result interface{}) (err error) { var hashResult string err = r.CallContext(ctx, &hashResult, "chain_getFinalizedHead") From e40616fe6287be5aee6cf1018fa1f98fab5e9991 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Mon, 9 Sep 2024 17:35:34 -0500 Subject: [PATCH 02/27] add new flag, NewHeadsPollInterval --- .changeset/happy-feet-rhyme.md | 5 +++++ core/chains/evm/client/config_builder.go | 3 ++- core/chains/evm/client/config_builder_test.go | 5 ++++- core/chains/evm/client/evm_client.go | 4 ++-- core/chains/evm/client/evm_client_test.go | 4 +++- core/chains/evm/client/helpers_test.go | 9 ++++++-- core/chains/evm/client/rpc_client.go | 3 +++ core/chains/evm/client/rpc_client_test.go | 22 +++++++++---------- .../evm/config/chain_scoped_node_pool.go | 4 ++++ core/chains/evm/config/config.go | 1 + core/chains/evm/config/toml/config.go | 6 +++++ core/services/chainlink/config_test.go | 1 + 12 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 .changeset/happy-feet-rhyme.md diff --git a/.changeset/happy-feet-rhyme.md b/.changeset/happy-feet-rhyme.md new file mode 100644 index 00000000000..011aace101d --- /dev/null +++ b/.changeset/happy-feet-rhyme.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Add a new flag support called `NewHeadsPollInterval`, which is an interval for polling new block periodically using http client rather than subscribe to ws feed. #added diff --git a/core/chains/evm/client/config_builder.go b/core/chains/evm/client/config_builder.go index fa702bac111..50aa8916d6b 100644 --- a/core/chains/evm/client/config_builder.go +++ b/core/chains/evm/client/config_builder.go @@ -43,7 +43,7 @@ func NewClientConfigs( deathDeclarationDelay time.Duration, noNewFinalizedHeadsThreshold time.Duration, finalizedBlockPollInterval time.Duration, - + newHeadsPollInterval time.Duration, ) (commonclient.ChainConfig, evmconfig.NodePool, []*toml.Node, error) { nodes, err := parseNodeConfigs(nodeCfgs) if err != nil { @@ -59,6 +59,7 @@ func NewClientConfigs( EnforceRepeatableRead: enforceRepeatableRead, DeathDeclarationDelay: commonconfig.MustNewDuration(deathDeclarationDelay), FinalizedBlockPollInterval: commonconfig.MustNewDuration(finalizedBlockPollInterval), + NewHeadsPollInterval: commonconfig.MustNewDuration(newHeadsPollInterval), } nodePoolCfg := &evmconfig.NodePoolConfig{C: nodePool} chainConfig := &evmconfig.EVMConfig{ diff --git a/core/chains/evm/client/config_builder_test.go b/core/chains/evm/client/config_builder_test.go index 403c6c2d619..515d7202220 100644 --- a/core/chains/evm/client/config_builder_test.go +++ b/core/chains/evm/client/config_builder_test.go @@ -37,9 +37,11 @@ func TestClientConfigBuilder(t *testing.T) { finalityDepth := ptr(uint32(10)) finalityTagEnabled := ptr(true) noNewHeadsThreshold := time.Second + newHeadsPollInterval := 12 * time.Second chainCfg, nodePool, nodes, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs, pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth, - finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, pollInterval) + finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, + pollInterval, newHeadsPollInterval) require.NoError(t, err) // Validate node pool configs @@ -52,6 +54,7 @@ func TestClientConfigBuilder(t *testing.T) { require.Equal(t, *enforceRepeatableRead, nodePool.EnforceRepeatableRead()) require.Equal(t, deathDeclarationDelay, nodePool.DeathDeclarationDelay()) require.Equal(t, pollInterval, nodePool.FinalizedBlockPollInterval()) + require.Equal(t, newHeadsPollInterval, nodePool.NewHeadsPollInterval()) // Validate node configs require.Equal(t, *nodeConfigs[0].Name, *nodes[0].Name) diff --git a/core/chains/evm/client/evm_client.go b/core/chains/evm/client/evm_client.go index 1fd533d6aab..c26362d6351 100644 --- a/core/chains/evm/client/evm_client.go +++ b/core/chains/evm/client/evm_client.go @@ -22,13 +22,13 @@ func NewEvmClient(cfg evmconfig.NodePool, chainCfg commonclient.ChainConfig, cli for i, node := range nodes { if node.SendOnly != nil && *node.SendOnly { rpc := NewRPCClient(lggr, empty, (*url.URL)(node.HTTPURL), *node.Name, int32(i), chainID, - commonclient.Secondary, cfg.FinalizedBlockPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType) + commonclient.Secondary, cfg.FinalizedBlockPollInterval(), cfg.NewHeadsPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType) sendonly := commonclient.NewSendOnlyNode(lggr, (url.URL)(*node.HTTPURL), *node.Name, chainID, rpc) sendonlys = append(sendonlys, sendonly) } else { rpc := NewRPCClient(lggr, (url.URL)(*node.WSURL), (*url.URL)(node.HTTPURL), *node.Name, int32(i), - chainID, commonclient.Primary, cfg.FinalizedBlockPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType) + chainID, commonclient.Primary, cfg.FinalizedBlockPollInterval(), cfg.NewHeadsPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType) primaryNode := commonclient.NewNode(cfg, chainCfg, lggr, (url.URL)(*node.WSURL), (*url.URL)(node.HTTPURL), *node.Name, int32(i), chainID, *node.Order, rpc, "EVM") diff --git a/core/chains/evm/client/evm_client_test.go b/core/chains/evm/client/evm_client_test.go index bdfcf426744..b762c14653c 100644 --- a/core/chains/evm/client/evm_client_test.go +++ b/core/chains/evm/client/evm_client_test.go @@ -29,6 +29,7 @@ func TestNewEvmClient(t *testing.T) { deathDeclarationDelay := time.Second * 3 noNewFinalizedBlocksThreshold := time.Second * 5 finalizedBlockPollInterval := time.Second * 4 + newHeadsPollInterval := time.Second * 4 nodeConfigs := []client.NodeConfig{ { Name: ptr("foo"), @@ -40,7 +41,8 @@ func TestNewEvmClient(t *testing.T) { finalityTagEnabled := ptr(true) chainCfg, nodePool, nodes, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs, pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth, - finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, finalizedBlockPollInterval) + finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, + finalizedBlockPollInterval, newHeadsPollInterval) require.NoError(t, err) client := client.NewEvmClient(nodePool, chainCfg, nil, logger.Test(t), testutils.FixtureChainID, nodes, chaintype.ChainType(chainTypeStr)) diff --git a/core/chains/evm/client/helpers_test.go b/core/chains/evm/client/helpers_test.go index 67977b180ed..031f6481574 100644 --- a/core/chains/evm/client/helpers_test.go +++ b/core/chains/evm/client/helpers_test.go @@ -92,6 +92,7 @@ type TestNodePoolConfig struct { NodeErrors config.ClientErrors EnforceRepeatableReadVal bool NodeDeathDeclarationDelay time.Duration + NodeNewHeadsPollInterval time.Duration } func (tc TestNodePoolConfig) PollFailureThreshold() uint32 { return tc.NodePollFailureThreshold } @@ -110,6 +111,10 @@ func (tc TestNodePoolConfig) FinalizedBlockPollInterval() time.Duration { return tc.NodeFinalizedBlockPollInterval } +func (tc TestNodePoolConfig) NewHeadsPollInterval() time.Duration { + return tc.NodeNewHeadsPollInterval +} + func (tc TestNodePoolConfig) Errors() config.ClientErrors { return tc.NodeErrors } @@ -143,7 +148,7 @@ func NewChainClientWithTestNode( } lggr := logger.Test(t) - rpc := NewRPCClient(lggr, *parsed, rpcHTTPURL, "eth-primary-rpc-0", id, chainID, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := NewRPCClient(lggr, *parsed, rpcHTTPURL, "eth-primary-rpc-0", id, chainID, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") n := commonclient.NewNode[*big.Int, *evmtypes.Head, RPCClient]( nodeCfg, clientMocks.ChainConfig{NoNewHeadsThresholdVal: noNewHeadsThreshold}, lggr, *parsed, rpcHTTPURL, "eth-primary-node-0", id, chainID, 1, rpc, "EVM") @@ -155,7 +160,7 @@ func NewChainClientWithTestNode( return nil, pkgerrors.Errorf("sendonly ethereum rpc url scheme must be http(s): %s", u.String()) } var empty url.URL - rpc := NewRPCClient(lggr, empty, &sendonlyRPCURLs[i], fmt.Sprintf("eth-sendonly-rpc-%d", i), id, chainID, commonclient.Secondary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := NewRPCClient(lggr, empty, &sendonlyRPCURLs[i], fmt.Sprintf("eth-sendonly-rpc-%d", i), id, chainID, commonclient.Secondary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") s := commonclient.NewSendOnlyNode[*big.Int, RPCClient]( lggr, u, fmt.Sprintf("eth-sendonly-%d", i), chainID, rpc) sendonlys = append(sendonlys, s) diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 0752def9949..7c3a862b249 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -123,6 +123,7 @@ type rpcClient struct { largePayloadRpcTimeout time.Duration rpcTimeout time.Duration finalizedBlockPollInterval time.Duration + newHeadsPollInterval time.Duration chainType chaintype.ChainType ws rawclient @@ -159,6 +160,7 @@ func NewRPCClient( chainID *big.Int, tier commonclient.NodeTier, finalizedBlockPollInterval time.Duration, + newHeadsPollInterval time.Duration, largePayloadRpcTimeout time.Duration, rpcTimeout time.Duration, chainType chaintype.ChainType, @@ -174,6 +176,7 @@ func NewRPCClient( r.tier = tier r.ws.uri = wsuri r.finalizedBlockPollInterval = finalizedBlockPollInterval + r.newHeadsPollInterval = newHeadsPollInterval if httpuri != nil { r.http = &rawclient{uri: *httpuri} } diff --git a/core/chains/evm/client/rpc_client_test.go b/core/chains/evm/client/rpc_client_test.go index 07e097727a3..b594a0ca166 100644 --- a/core/chains/evm/client/rpc_client_test.go +++ b/core/chains/evm/client/rpc_client_test.go @@ -61,7 +61,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) // set to default values @@ -111,7 +111,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) ch := make(chan *evmtypes.Head) @@ -136,7 +136,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) var wg sync.WaitGroup @@ -160,7 +160,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { t.Run("Block's chain ID matched configured", func(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) ch := make(chan *evmtypes.Head) @@ -177,7 +177,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { }) wsURL := server.WSURL() observedLggr, observed := logger.TestObserved(t, zap.DebugLevel) - rpc := client.NewRPCClient(observedLggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(observedLggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") require.NoError(t, rpc.Dial(ctx)) server.Close() _, err := rpc.SubscribeNewHead(ctx, make(chan *evmtypes.Head)) @@ -187,7 +187,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { t.Run("Subscription error is properly wrapper", func(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) sub, err := rpc.SubscribeNewHead(ctx, make(chan *evmtypes.Head)) @@ -215,7 +215,7 @@ func TestRPCClient_SubscribeFilterLogs(t *testing.T) { }) wsURL := server.WSURL() observedLggr, observed := logger.TestObserved(t, zap.DebugLevel) - rpc := client.NewRPCClient(observedLggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(observedLggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") require.NoError(t, rpc.Dial(ctx)) server.Close() _, err := rpc.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log)) @@ -232,7 +232,7 @@ func TestRPCClient_SubscribeFilterLogs(t *testing.T) { return resp }) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) sub, err := rpc.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log)) @@ -281,7 +281,7 @@ func TestRPCClient_LatestFinalizedBlock(t *testing.T) { } server := createRPCServer() - rpc := client.NewRPCClient(lggr, *server.URL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, *server.URL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") require.NoError(t, rpc.Dial(ctx)) defer rpc.Close() server.Head = &evmtypes.Head{Number: 128} @@ -391,7 +391,7 @@ func TestRpcClientLargePayloadTimeout(t *testing.T) { // use something unreasonably large for RPC timeout to ensure that we use largePayloadRPCTimeout const rpcTimeout = time.Hour const largePayloadRPCTimeout = tests.TestInterval - rpc := client.NewRPCClient(logger.Test(t), *rpcURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, largePayloadRPCTimeout, rpcTimeout, "") + rpc := client.NewRPCClient(logger.Test(t), *rpcURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, largePayloadRPCTimeout, rpcTimeout, "") require.NoError(t, rpc.Dial(ctx)) defer rpc.Close() err := testCase.Fn(ctx, rpc) @@ -431,7 +431,7 @@ func TestAstarCustomFinality(t *testing.T) { const expectedFinalizedBlockNumber = int64(4) const expectedFinalizedBlockHash = "0x7441e97acf83f555e0deefef86db636bc8a37eb84747603412884e4df4d22804" - rpcClient := client.NewRPCClient(logger.Test(t), *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, chaintype.ChainAstar) + rpcClient := client.NewRPCClient(logger.Test(t), *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, chaintype.ChainAstar) defer rpcClient.Close() err := rpcClient.Dial(tests.Context(t)) require.NoError(t, err) diff --git a/core/chains/evm/config/chain_scoped_node_pool.go b/core/chains/evm/config/chain_scoped_node_pool.go index a4974366486..4b1d02d148e 100644 --- a/core/chains/evm/config/chain_scoped_node_pool.go +++ b/core/chains/evm/config/chain_scoped_node_pool.go @@ -38,6 +38,10 @@ func (n *NodePoolConfig) FinalizedBlockPollInterval() time.Duration { return n.C.FinalizedBlockPollInterval.Duration() } +func (n *NodePoolConfig) NewHeadsPollInterval() time.Duration { + return n.C.NewHeadsPollInterval.Duration() +} + func (n *NodePoolConfig) Errors() ClientErrors { return &clientErrorsConfig{c: n.C.Errors} } func (n *NodePoolConfig) EnforceRepeatableRead() bool { diff --git a/core/chains/evm/config/config.go b/core/chains/evm/config/config.go index 943616d9630..e5b806aa58c 100644 --- a/core/chains/evm/config/config.go +++ b/core/chains/evm/config/config.go @@ -182,6 +182,7 @@ type NodePool interface { Errors() ClientErrors EnforceRepeatableRead() bool DeathDeclarationDelay() time.Duration + NewHeadsPollInterval() time.Duration } // TODO BCF-2509 does the chainscopedconfig really need the entire app config? diff --git a/core/chains/evm/config/toml/config.go b/core/chains/evm/config/toml/config.go index ecc278a7ec5..869d2f25e57 100644 --- a/core/chains/evm/config/toml/config.go +++ b/core/chains/evm/config/toml/config.go @@ -885,6 +885,7 @@ type NodePool struct { Errors ClientErrors `toml:",omitempty"` EnforceRepeatableRead *bool DeathDeclarationDelay *commonconfig.Duration + NewHeadsPollInterval *commonconfig.Duration } func (p *NodePool) setFrom(f *NodePool) { @@ -917,6 +918,11 @@ func (p *NodePool) setFrom(f *NodePool) { if v := f.DeathDeclarationDelay; v != nil { p.DeathDeclarationDelay = v } + + if v := f.NewHeadsPollInterval; v != nil { + p.NewHeadsPollInterval = v + } + p.Errors.setFrom(&f.Errors) } diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index 71dc763ad6a..345175bee25 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -626,6 +626,7 @@ func TestConfig_Marshal(t *testing.T) { FinalizedBlockPollInterval: &second, EnforceRepeatableRead: ptr(true), DeathDeclarationDelay: &minute, + NewHeadsPollInterval: &second, Errors: evmcfg.ClientErrors{ NonceTooLow: ptr[string]("(: |^)nonce too low"), NonceTooHigh: ptr[string]("(: |^)nonce too high"), From 42c48aec697ede9e5f783ded9bc6f3d08678acb4 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Tue, 10 Sep 2024 16:14:23 -0500 Subject: [PATCH 03/27] fix test --- core/chains/evm/client/config_builder_test.go | 2 +- core/chains/evm/config/toml/defaults/fallback.toml | 1 + core/config/docs/chains-evm.toml | 4 ++++ core/services/chainlink/config_test.go | 3 ++- core/services/chainlink/testdata/config-full.toml | 1 + .../chainlink/testdata/config-multi-chain-effective.toml | 3 +++ 6 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/chains/evm/client/config_builder_test.go b/core/chains/evm/client/config_builder_test.go index 515d7202220..28620ac6ca9 100644 --- a/core/chains/evm/client/config_builder_test.go +++ b/core/chains/evm/client/config_builder_test.go @@ -37,7 +37,7 @@ func TestClientConfigBuilder(t *testing.T) { finalityDepth := ptr(uint32(10)) finalityTagEnabled := ptr(true) noNewHeadsThreshold := time.Second - newHeadsPollInterval := 12 * time.Second + newHeadsPollInterval := 0 * time.Second chainCfg, nodePool, nodes, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs, pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth, finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, diff --git a/core/chains/evm/config/toml/defaults/fallback.toml b/core/chains/evm/config/toml/defaults/fallback.toml index 2f9af4f85a8..6f43f956faf 100644 --- a/core/chains/evm/config/toml/defaults/fallback.toml +++ b/core/chains/evm/config/toml/defaults/fallback.toml @@ -77,6 +77,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 diff --git a/core/config/docs/chains-evm.toml b/core/config/docs/chains-evm.toml index ce85f242f5d..bab4385f9fb 100644 --- a/core/config/docs/chains-evm.toml +++ b/core/config/docs/chains-evm.toml @@ -409,6 +409,10 @@ EnforceRepeatableRead = false # Default # trigger declaration of `FinalizedBlockOutOfSync` due to insignificant network delays in broadcasting of the finalized state among RPCs. # RPC will not be picked to handle a request even if this option is set to a nonzero value. DeathDeclarationDelay = '10s' # Default +# NewHeadsPollInterval define an interval for polling new block periodically using http client rather than subscribe to ws feed +# +# Set to 0 to disable. +NewHeadsPollInterval = '0s' # Default # **ADVANCED** # Errors enable the node to provide custom regex patterns to match against error messages from RPCs. [EVM.NodePool.Errors] diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index 345175bee25..9b93f17806e 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -626,7 +626,7 @@ func TestConfig_Marshal(t *testing.T) { FinalizedBlockPollInterval: &second, EnforceRepeatableRead: ptr(true), DeathDeclarationDelay: &minute, - NewHeadsPollInterval: &second, + NewHeadsPollInterval: &zeroSeconds, Errors: evmcfg.ClientErrors{ NonceTooLow: ptr[string]("(: |^)nonce too low"), NonceTooHigh: ptr[string]("(: |^)nonce too high"), @@ -1110,6 +1110,7 @@ NodeIsSyncingEnabled = true FinalizedBlockPollInterval = '1s' EnforceRepeatableRead = true DeathDeclarationDelay = '1m0s' +NewHeadsPollInterval = '0s' [EVM.NodePool.Errors] NonceTooLow = '(: |^)nonce too low' diff --git a/core/services/chainlink/testdata/config-full.toml b/core/services/chainlink/testdata/config-full.toml index 0474a5a1e31..e78b91a5749 100644 --- a/core/services/chainlink/testdata/config-full.toml +++ b/core/services/chainlink/testdata/config-full.toml @@ -392,6 +392,7 @@ NodeIsSyncingEnabled = true FinalizedBlockPollInterval = '1s' EnforceRepeatableRead = true DeathDeclarationDelay = '1m0s' +NewHeadsPollInterval = '0s' [EVM.NodePool.Errors] NonceTooLow = '(: |^)nonce too low' diff --git a/core/services/chainlink/testdata/config-multi-chain-effective.toml b/core/services/chainlink/testdata/config-multi-chain-effective.toml index 5472be09bfb..2f21fe04fcc 100644 --- a/core/services/chainlink/testdata/config-multi-chain-effective.toml +++ b/core/services/chainlink/testdata/config-multi-chain-effective.toml @@ -363,6 +363,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 @@ -471,6 +472,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 @@ -573,6 +575,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 From 035b0859b1d4259203e62b6acbd372f61e39f497 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Tue, 10 Sep 2024 17:31:57 -0500 Subject: [PATCH 04/27] fix test --- core/web/resolver/testdata/config-full.toml | 1 + .../config-multi-chain-effective.toml | 3 + docs/CONFIG.md | 70 +++++++++++++++++++ .../node/validate/defaults-override.txtar | 1 + .../disk-based-logging-disabled.txtar | 1 + .../validate/disk-based-logging-no-dir.txtar | 1 + .../node/validate/disk-based-logging.txtar | 1 + testdata/scripts/node/validate/invalid.txtar | 1 + testdata/scripts/node/validate/valid.txtar | 1 + 9 files changed, 80 insertions(+) diff --git a/core/web/resolver/testdata/config-full.toml b/core/web/resolver/testdata/config-full.toml index c01ab09b666..d3f80ffbf1f 100644 --- a/core/web/resolver/testdata/config-full.toml +++ b/core/web/resolver/testdata/config-full.toml @@ -391,6 +391,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.NodePool.Errors] NonceTooLow = '(: |^)nonce too low' diff --git a/core/web/resolver/testdata/config-multi-chain-effective.toml b/core/web/resolver/testdata/config-multi-chain-effective.toml index e631642e051..654d5f82b40 100644 --- a/core/web/resolver/testdata/config-multi-chain-effective.toml +++ b/core/web/resolver/testdata/config-multi-chain-effective.toml @@ -363,6 +363,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 @@ -471,6 +472,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 @@ -573,6 +575,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 3b42bc37562..8965fd6ddcf 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -1984,6 +1984,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2086,6 +2087,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2188,6 +2190,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2290,6 +2293,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2393,6 +2397,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -2495,6 +2500,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2597,6 +2603,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2700,6 +2707,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2802,6 +2810,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2903,6 +2912,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3004,6 +3014,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3106,6 +3117,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3209,6 +3221,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3311,6 +3324,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3413,6 +3427,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3515,6 +3530,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -3617,6 +3633,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -3719,6 +3736,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3821,6 +3839,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -3923,6 +3942,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -4025,6 +4045,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -4127,6 +4148,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -4230,6 +4252,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4332,6 +4355,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4433,6 +4457,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4535,6 +4560,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4637,6 +4663,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4739,6 +4766,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4841,6 +4869,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4942,6 +4971,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5044,6 +5074,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5146,6 +5177,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5248,6 +5280,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5350,6 +5383,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -5451,6 +5485,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5553,6 +5588,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5655,6 +5691,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -5758,6 +5795,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5861,6 +5899,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5964,6 +6003,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6066,6 +6106,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6168,6 +6209,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6270,6 +6312,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6372,6 +6415,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6473,6 +6517,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -6574,6 +6619,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -6675,6 +6721,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -6777,6 +6824,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6879,6 +6927,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -6980,6 +7029,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -7082,6 +7132,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7184,6 +7235,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7287,6 +7339,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7390,6 +7443,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7492,6 +7546,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7594,6 +7649,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7696,6 +7752,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7798,6 +7855,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -7900,6 +7958,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -8002,6 +8061,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -8104,6 +8164,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -8860,6 +8921,7 @@ NodeIsSyncingEnabled = false # Default FinalizedBlockPollInterval = '5s' # Default EnforceRepeatableRead = false # Default DeathDeclarationDelay = '10s' # Default +NewHeadsPollInterval = '0s' # Default ``` The node pool manages multiple RPC endpoints. @@ -8952,6 +9014,14 @@ Larger values might be helpful to reduce the noisiness of health checks like `En trigger declaration of `FinalizedBlockOutOfSync` due to insignificant network delays in broadcasting of the finalized state among RPCs. RPC will not be picked to handle a request even if this option is set to a nonzero value. +### NewHeadsPollInterval +```toml +NewHeadsPollInterval = '0s' # Default +``` +NewHeadsPollInterval define an interval for polling new block periodically using http client rather than subscribe to ws feed + +Set to 0 to disable. + ## EVM.NodePool.Errors :warning: **_ADVANCED_**: _Do not change these settings unless you know what you are doing._ ```toml diff --git a/testdata/scripts/node/validate/defaults-override.txtar b/testdata/scripts/node/validate/defaults-override.txtar index 07bdea1ce16..e911ae9bc09 100644 --- a/testdata/scripts/node/validate/defaults-override.txtar +++ b/testdata/scripts/node/validate/defaults-override.txtar @@ -436,6 +436,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar index 6555292405e..93068e34f37 100644 --- a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar @@ -419,6 +419,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar index 93769eb6c6b..1b20b1be8f2 100644 --- a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar @@ -419,6 +419,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/testdata/scripts/node/validate/disk-based-logging.txtar b/testdata/scripts/node/validate/disk-based-logging.txtar index eaf38bf2a53..4c835526b2f 100644 --- a/testdata/scripts/node/validate/disk-based-logging.txtar +++ b/testdata/scripts/node/validate/disk-based-logging.txtar @@ -419,6 +419,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/testdata/scripts/node/validate/invalid.txtar b/testdata/scripts/node/validate/invalid.txtar index 4838ddc61c2..b62c6e382e5 100644 --- a/testdata/scripts/node/validate/invalid.txtar +++ b/testdata/scripts/node/validate/invalid.txtar @@ -409,6 +409,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/testdata/scripts/node/validate/valid.txtar b/testdata/scripts/node/validate/valid.txtar index f7349ed533c..2996f5935d2 100644 --- a/testdata/scripts/node/validate/valid.txtar +++ b/testdata/scripts/node/validate/valid.txtar @@ -416,6 +416,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 From 5090efcbe6342c685a0ffc89e9be439a1d3c062e Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Wed, 11 Sep 2024 10:15:15 -0500 Subject: [PATCH 05/27] step for adding polling new head impelmentation and sync+outOfSync loop handling logic. To add unit test --- common/client/mock_node_client_test.go | 22 ++--- common/client/mock_rpc_test.go | 22 ++--- common/client/node.go | 1 + common/client/node_lifecycle.go | 93 ++++++++++++++++++++++ common/client/types.go | 2 +- core/chains/evm/client/mocks/rpc_client.go | 22 ++--- core/chains/evm/client/rpc_client.go | 10 ++- 7 files changed, 135 insertions(+), 37 deletions(-) diff --git a/common/client/mock_node_client_test.go b/common/client/mock_node_client_test.go index ab1aec77c05..07ed5cb31da 100644 --- a/common/client/mock_node_client_test.go +++ b/common/client/mock_node_client_test.go @@ -626,12 +626,12 @@ func (_c *mockNodeClient_SubscribeToHeads_Call[CHAIN_ID, HEAD]) RunAndReturn(run return _c } -// SubscribeToNewHeads provides a mock function with given fields: _a0 -func (_m *mockNodeClient[CHAIN_ID, HEAD]) SubscribeToNewHeads(_a0 context.Context) (<-chan HEAD, types.Subscription, error) { +// SubscribeToPollingNewHeads provides a mock function with given fields: _a0 +func (_m *mockNodeClient[CHAIN_ID, HEAD]) SubscribeToPollingNewHeads(_a0 context.Context) (<-chan HEAD, types.Subscription, error) { ret := _m.Called(_a0) if len(ret) == 0 { - panic("no return value specified for SubscribeToNewHeads") + panic("no return value specified for SubscribeToPollingNewHeads") } var r0 <-chan HEAD @@ -665,30 +665,30 @@ func (_m *mockNodeClient[CHAIN_ID, HEAD]) SubscribeToNewHeads(_a0 context.Contex return r0, r1, r2 } -// mockNodeClient_SubscribeToNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToNewHeads' -type mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID types.ID, HEAD Head] struct { +// mockNodeClient_SubscribeToPollingNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToPollingNewHeads' +type mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID types.ID, HEAD Head] struct { *mock.Call } -// SubscribeToNewHeads is a helper method to define mock.On call +// SubscribeToPollingNewHeads is a helper method to define mock.On call // - _a0 context.Context -func (_e *mockNodeClient_Expecter[CHAIN_ID, HEAD]) SubscribeToNewHeads(_a0 interface{}) *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD] { - return &mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD]{Call: _e.mock.On("SubscribeToNewHeads", _a0)} +func (_e *mockNodeClient_Expecter[CHAIN_ID, HEAD]) SubscribeToPollingNewHeads(_a0 interface{}) *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD] { + return &mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD]{Call: _e.mock.On("SubscribeToPollingNewHeads", _a0)} } -func (_c *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD]) Run(run func(_a0 context.Context)) *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD] { +func (_c *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD]) Run(run func(_a0 context.Context)) *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD] { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context)) }) return _c } -func (_c *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD]) Return(_a0 <-chan HEAD, _a1 types.Subscription, _a2 error) *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD] { +func (_c *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD]) Return(_a0 <-chan HEAD, _a1 types.Subscription, _a2 error) *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD] { _c.Call.Return(_a0, _a1, _a2) return _c } -func (_c *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD]) RunAndReturn(run func(context.Context) (<-chan HEAD, types.Subscription, error)) *mockNodeClient_SubscribeToNewHeads_Call[CHAIN_ID, HEAD] { +func (_c *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD]) RunAndReturn(run func(context.Context) (<-chan HEAD, types.Subscription, error)) *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD] { _c.Call.Return(run) return _c } diff --git a/common/client/mock_rpc_test.go b/common/client/mock_rpc_test.go index 418e825b2d7..46f6b9179a3 100644 --- a/common/client/mock_rpc_test.go +++ b/common/client/mock_rpc_test.go @@ -1708,12 +1708,12 @@ func (_c *mockRPC_SubscribeToHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_ return _c } -// SubscribeToNewHeads provides a mock function with given fields: _a0 -func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) SubscribeToNewHeads(_a0 context.Context) (<-chan HEAD, types.Subscription, error) { +// SubscribeToPollingNewHeads provides a mock function with given fields: _a0 +func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) SubscribeToPollingNewHeads(_a0 context.Context) (<-chan HEAD, types.Subscription, error) { ret := _m.Called(_a0) if len(ret) == 0 { - panic("no return value specified for SubscribeToNewHeads") + panic("no return value specified for SubscribeToPollingNewHeads") } var r0 <-chan HEAD @@ -1747,30 +1747,30 @@ func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS return r0, r1, r2 } -// mockRPC_SubscribeToNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToNewHeads' -type mockRPC_SubscribeToNewHeads_Call[CHAIN_ID types.ID, SEQ types.Sequence, ADDR types.Hashable, BLOCK_HASH types.Hashable, TX interface{}, TX_HASH types.Hashable, EVENT interface{}, EVENT_OPS interface{}, TX_RECEIPT types.Receipt[TX_HASH, BLOCK_HASH], FEE feetypes.Fee, HEAD types.Head[BLOCK_HASH], BATCH_ELEM interface{}] struct { +// mockRPC_SubscribeToPollingNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToPollingNewHeads' +type mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID types.ID, SEQ types.Sequence, ADDR types.Hashable, BLOCK_HASH types.Hashable, TX interface{}, TX_HASH types.Hashable, EVENT interface{}, EVENT_OPS interface{}, TX_RECEIPT types.Receipt[TX_HASH, BLOCK_HASH], FEE feetypes.Fee, HEAD types.Head[BLOCK_HASH], BATCH_ELEM interface{}] struct { *mock.Call } -// SubscribeToNewHeads is a helper method to define mock.On call +// SubscribeToPollingNewHeads is a helper method to define mock.On call // - _a0 context.Context -func (_e *mockRPC_Expecter[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) SubscribeToNewHeads(_a0 interface{}) *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { - return &mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]{Call: _e.mock.On("SubscribeToNewHeads", _a0)} +func (_e *mockRPC_Expecter[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) SubscribeToPollingNewHeads(_a0 interface{}) *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { + return &mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]{Call: _e.mock.On("SubscribeToPollingNewHeads", _a0)} } -func (_c *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Run(run func(_a0 context.Context)) *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { +func (_c *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Run(run func(_a0 context.Context)) *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context)) }) return _c } -func (_c *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Return(_a0 <-chan HEAD, _a1 types.Subscription, _a2 error) *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { +func (_c *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Return(_a0 <-chan HEAD, _a1 types.Subscription, _a2 error) *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { _c.Call.Return(_a0, _a1, _a2) return _c } -func (_c *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) RunAndReturn(run func(context.Context) (<-chan HEAD, types.Subscription, error)) *mockRPC_SubscribeToNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { +func (_c *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) RunAndReturn(run func(context.Context) (<-chan HEAD, types.Subscription, error)) *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { _c.Call.Return(run) return _c } diff --git a/common/client/node.go b/common/client/node.go index d6543c772a8..1f55e69cacc 100644 --- a/common/client/node.go +++ b/common/client/node.go @@ -45,6 +45,7 @@ type NodeConfig interface { FinalizedBlockPollInterval() time.Duration EnforceRepeatableRead() bool DeathDeclarationDelay() time.Duration + NewHeadsPollInterval() time.Duration } type ChainConfig interface { diff --git a/common/client/node_lifecycle.go b/common/client/node_lifecycle.go index 40d9a9ef6ef..66a00b74a20 100644 --- a/common/client/node_lifecycle.go +++ b/common/client/node_lifecycle.go @@ -91,6 +91,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { noNewFinalizedBlocksTimeoutThreshold := n.chainCfg.NoNewFinalizedHeadsThreshold() pollFailureThreshold := n.nodePoolCfg.PollFailureThreshold() pollInterval := n.nodePoolCfg.PollInterval() + newHeadsPollInterval := n.nodePoolCfg.NewHeadsPollInterval() lggr := logger.Sugared(n.lfcLog).Named("Alive").With("noNewHeadsTimeoutThreshold", noNewHeadsTimeoutThreshold, "pollInterval", pollInterval, "pollFailureThreshold", pollFailureThreshold) lggr.Tracew("Alive loop starting", "nodeState", n.getCachedState()) @@ -121,6 +122,19 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { defer finalizedHeadsSub.Unsubscribe() } + var pollingNewHeadsSub headSubscription[HEAD] + if newHeadsPollInterval > 0 { + pollingNewHeadsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "newHeadsPoll"), + noNewHeadsTimeoutThreshold, n.rpc.SubscribeToPollingNewHeads) + if err != nil { + lggr.Errorw("Failed to subscribe to polling new heads", "err", err) + n.declareUnreachable() + return + } + + defer pollingNewHeadsSub.Unsubscribe() + } + var pollCh <-chan time.Time if pollInterval > 0 { lggr.Debug("Polling enabled") @@ -247,6 +261,36 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { lggr.Errorw("Finalized heads subscription was terminated", "err", err) n.declareUnreachable() return + case newHead, open := <-pollingNewHeadsSub.Heads: + if !open { + lggr.Errorw("polling new heads subscription channel unexpectedly closed", "nodeState", n.getCachedState()) + n.declareUnreachable() + return + } + + receivedNewHead := n.onNewHead(lggr, &localHighestChainInfo, newHead) + if receivedNewHead && noNewHeadsTimeoutThreshold > 0 { + pollingNewHeadsSub.ResetTimer(noNewHeadsTimeoutThreshold) + } + case <-pollingNewHeadsSub.NoNewHeads: + // We haven't received a head on the channel for at least the + // threshold amount of time, mark it broken + lggr.Errorw(fmt.Sprintf("RPC's new heads with polling state is out of sync; no new heads received for %s", noNewFinalizedBlocksTimeoutThreshold), "latestReceivedBlockNumber", localHighestChainInfo.BlockNumber) + if n.poolInfoProvider != nil { + if l, _ := n.poolInfoProvider.LatestChainInfo(); l < 2 { + lggr.Criticalf("RPC's new heads with polling state is out of sync; %s %s", msgCannotDisable, msgDegradedState) + // We don't necessarily want to wait the full timeout to check again, we should + // check regularly and log noisily in this state + pollingNewHeadsSub.ResetTimer(zombieNodeCheckInterval(noNewHeadsTimeoutThreshold)) + continue + } + } + n.declareOutOfSync(syncStatusNoNewFinalizedHead) + return + case <-pollingNewHeadsSub.Errors: + lggr.Errorw("new heads polling subscription was terminated", "err", err, "nodeState", n.getCachedState()) + n.declareUnreachable() + return } } } @@ -379,6 +423,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) isOutOfSyncWithPool(localState ChainInfo) (o const ( msgReceivedBlock = "Received block for RPC node, waiting until back in-sync to mark as live again" msgReceivedFinalizedBlock = "Received new finalized block for RPC node, waiting until back in-sync to mark as live again" + msgReceivedPollingBlock = "Received polling block for RPC node, waiting until back in-sync to mark as live again" msgInSync = "RPC node back in sync" ) @@ -440,6 +485,20 @@ func (n *node[CHAIN_ID, HEAD, RPC]) outOfSyncLoop(syncIssues syncStatus) { defer finalizedHeadsSub.Unsubscribe() } + var pollingNewHeadsSub headSubscription[HEAD] + if n.nodePoolCfg.NewHeadsPollInterval() > 0 { + pollingNewHeadsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "newHeadsPoll"), + noNewHeadsTimeoutThreshold, n.rpc.SubscribeToPollingNewHeads) + if err != nil { + lggr.Errorw("Failed to subscribe to polling new heads on out-of-sync RPC node", "err", err) + n.declareUnreachable() + return + } + + lggr.Tracew("Successfully subscribed to polling new heads feed on out-of-sync RPC node") + defer pollingNewHeadsSub.Unsubscribe() + } + _, localHighestChainInfo := n.rpc.GetInterceptedChainInfo() for { if syncIssues == syncStatusSynced { @@ -525,6 +584,40 @@ func (n *node[CHAIN_ID, HEAD, RPC]) outOfSyncLoop(syncIssues syncStatus) { // we are not resetting the timer, as there is no need to add syncStatusNoNewFinalizedHead until it's removed on new finalized head. syncIssues |= syncStatusNoNewFinalizedHead lggr.Debugw(fmt.Sprintf("No new finalized heads received for %s. Node stays out-of-sync due to sync issues: %s", noNewFinalizedBlocksTimeoutThreshold, syncIssues)) + case head, open := <-pollingNewHeadsSub.Heads: + if !open { + lggr.Errorw("polling new heads subscription channel unexpectedly closed", "nodeState", n.getCachedState()) + n.declareUnreachable() + return + } + + if !n.onNewHead(lggr, &localHighestChainInfo, head) { + continue + } + + // received a new head - clear NoNewHead flag + syncIssues &= ^syncStatusNoNewHead + if outOfSync, _ := n.isOutOfSyncWithPool(localHighestChainInfo); !outOfSync { + // we caught up with the pool - clear NotInSyncWithPool flag + syncIssues &= ^syncStatusNotInSyncWithPool + } else { + // we've received new head, but lagging behind the pool, add NotInSyncWithPool flag to prevent false transition to alive + syncIssues |= syncStatusNotInSyncWithPool + } + + if noNewHeadsTimeoutThreshold > 0 { + pollingNewHeadsSub.ResetTimer(noNewHeadsTimeoutThreshold) + } + + lggr.Debugw(msgReceivedPollingBlock, "blockNumber", head.BlockNumber(), "blockDifficulty", head.BlockDifficulty(), "syncIssues", syncIssues) + case err = <-pollingNewHeadsSub.Errors: + lggr.Errorw("polling new heads subscription was terminated", "err", err) + n.declareUnreachable() + return + case <-pollingNewHeadsSub.NoNewHeads: + // we are not resetting the timer, as there is no need to add syncStatusNoNewHead until it's removed on new head. + syncIssues |= syncStatusNoNewHead + lggr.Debugw(fmt.Sprintf("No polling new heads received for %s. Node stays out-of-sync due to sync issues: %s", noNewHeadsTimeoutThreshold, syncIssues)) } } } diff --git a/common/client/types.go b/common/client/types.go index 2347361ba1a..8f6dd53ea76 100644 --- a/common/client/types.go +++ b/common/client/types.go @@ -80,7 +80,7 @@ type NodeClient[ // Ensure implementation does not have a race condition when values are reset before request completion and as // a result latest ChainInfo contains information from the previous cycle. GetInterceptedChainInfo() (latest, highestUserObservations ChainInfo) - SubscribeToNewHeads(_ context.Context) (<-chan HEAD, types.Subscription, error) + SubscribeToPollingNewHeads(_ context.Context) (<-chan HEAD, types.Subscription, error) } // clientAPI includes all the direct RPC methods required by the generalized common client to implement its own. diff --git a/core/chains/evm/client/mocks/rpc_client.go b/core/chains/evm/client/mocks/rpc_client.go index 0c7ff4a9f81..4931274baed 100644 --- a/core/chains/evm/client/mocks/rpc_client.go +++ b/core/chains/evm/client/mocks/rpc_client.go @@ -2145,12 +2145,12 @@ func (_c *RPCClient_SubscribeToHeads_Call) RunAndReturn(run func(context.Context return _c } -// SubscribeToNewHeads provides a mock function with given fields: _a0 -func (_m *RPCClient) SubscribeToNewHeads(_a0 context.Context) (<-chan *types.Head, commontypes.Subscription, error) { +// SubscribeToPollingNewHeads provides a mock function with given fields: _a0 +func (_m *RPCClient) SubscribeToPollingNewHeads(_a0 context.Context) (<-chan *types.Head, commontypes.Subscription, error) { ret := _m.Called(_a0) if len(ret) == 0 { - panic("no return value specified for SubscribeToNewHeads") + panic("no return value specified for SubscribeToPollingNewHeads") } var r0 <-chan *types.Head @@ -2184,30 +2184,30 @@ func (_m *RPCClient) SubscribeToNewHeads(_a0 context.Context) (<-chan *types.Hea return r0, r1, r2 } -// RPCClient_SubscribeToNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToNewHeads' -type RPCClient_SubscribeToNewHeads_Call struct { +// RPCClient_SubscribeToPollingNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToPollingNewHeads' +type RPCClient_SubscribeToPollingNewHeads_Call struct { *mock.Call } -// SubscribeToNewHeads is a helper method to define mock.On call +// SubscribeToPollingNewHeads is a helper method to define mock.On call // - _a0 context.Context -func (_e *RPCClient_Expecter) SubscribeToNewHeads(_a0 interface{}) *RPCClient_SubscribeToNewHeads_Call { - return &RPCClient_SubscribeToNewHeads_Call{Call: _e.mock.On("SubscribeToNewHeads", _a0)} +func (_e *RPCClient_Expecter) SubscribeToPollingNewHeads(_a0 interface{}) *RPCClient_SubscribeToPollingNewHeads_Call { + return &RPCClient_SubscribeToPollingNewHeads_Call{Call: _e.mock.On("SubscribeToPollingNewHeads", _a0)} } -func (_c *RPCClient_SubscribeToNewHeads_Call) Run(run func(_a0 context.Context)) *RPCClient_SubscribeToNewHeads_Call { +func (_c *RPCClient_SubscribeToPollingNewHeads_Call) Run(run func(_a0 context.Context)) *RPCClient_SubscribeToPollingNewHeads_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context)) }) return _c } -func (_c *RPCClient_SubscribeToNewHeads_Call) Return(_a0 <-chan *types.Head, _a1 commontypes.Subscription, _a2 error) *RPCClient_SubscribeToNewHeads_Call { +func (_c *RPCClient_SubscribeToPollingNewHeads_Call) Return(_a0 <-chan *types.Head, _a1 commontypes.Subscription, _a2 error) *RPCClient_SubscribeToPollingNewHeads_Call { _c.Call.Return(_a0, _a1, _a2) return _c } -func (_c *RPCClient_SubscribeToNewHeads_Call) RunAndReturn(run func(context.Context) (<-chan *types.Head, commontypes.Subscription, error)) *RPCClient_SubscribeToNewHeads_Call { +func (_c *RPCClient_SubscribeToPollingNewHeads_Call) RunAndReturn(run func(context.Context) (<-chan *types.Head, commontypes.Subscription, error)) *RPCClient_SubscribeToPollingNewHeads_Call { _c.Call.Return(run) return _c } diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 07d31e8b579..8fcecc7f98b 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -566,9 +566,13 @@ func (r *rpcClient) SubscribeToFinalizedHeads(ctx context.Context) (<-chan *evmt return channel, &poller, nil } -func (r *rpcClient) SubscribeToNewHeads(ctx context.Context) (<-chan *evmtypes.Head, commontypes.Subscription, error) { - // TODO to replace the interval and timeout values - poller, channel := commonclient.NewPoller[*evmtypes.Head](time.Second, r.LatestFinalizedBlock, time.Second, r.rpcLog) +func (r *rpcClient) SubscribeToPollingNewHeads(ctx context.Context) (<-chan *evmtypes.Head, commontypes.Subscription, error) { + interval := r.newHeadsPollInterval + if interval == 0 { + return nil, nil, errors.New("NewHeadsPollInterval is 0") + } + timeout := interval + poller, channel := commonclient.NewPoller[*evmtypes.Head](interval, r.LatestBlock, timeout, r.rpcLog) if err := poller.Start(ctx); err != nil { return nil, nil, err } From 1cd3f4c4319bb646a85794ea412ba38da9c8a762 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Wed, 11 Sep 2024 10:50:32 -0500 Subject: [PATCH 06/27] fix lint --- common/client/node_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/client/node_test.go b/common/client/node_test.go index 3b971e84902..66bb50fc94f 100644 --- a/common/client/node_test.go +++ b/common/client/node_test.go @@ -20,6 +20,11 @@ type testNodeConfig struct { enforceRepeatableRead bool finalizedBlockPollInterval time.Duration deathDeclarationDelay time.Duration + newHeadsPollInterval time.Duration +} + +func (n testNodeConfig) NewHeadsPollInterval() time.Duration { + return n.newHeadsPollInterval } func (n testNodeConfig) PollFailureThreshold() uint32 { From d5953518e1efa5b34d3ff2625ea46456f3cbbd90 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Wed, 11 Sep 2024 16:42:48 -0500 Subject: [PATCH 07/27] add unit test --- common/client/node_lifecycle.go | 105 +++++++++++------------ common/client/node_lifecycle_test.go | 119 ++++++++++++++++++++++++++- 2 files changed, 172 insertions(+), 52 deletions(-) diff --git a/common/client/node_lifecycle.go b/common/client/node_lifecycle.go index 66a00b74a20..749265579d1 100644 --- a/common/client/node_lifecycle.go +++ b/common/client/node_lifecycle.go @@ -96,18 +96,34 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { lggr := logger.Sugared(n.lfcLog).Named("Alive").With("noNewHeadsTimeoutThreshold", noNewHeadsTimeoutThreshold, "pollInterval", pollInterval, "pollFailureThreshold", pollFailureThreshold) lggr.Tracew("Alive loop starting", "nodeState", n.getCachedState()) - headsSub, err := n.registerNewSubscription(ctx, lggr.With("subscriptionType", "heads"), - n.chainCfg.NodeNoNewHeadsThreshold(), n.rpc.SubscribeToHeads) - if err != nil { - lggr.Errorw("Initial subscribe for heads failed", "nodeState", n.getCachedState(), "err", err) - n.declareUnreachable() - return - } + var err error + var headsSub, pollingNewHeadsSub headSubscription[HEAD] + // if new head based on http polling is enabled, we will skip regular newHead subscription + if newHeadsPollInterval > 0 { + pollingNewHeadsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "newHeadsPoll"), + noNewHeadsTimeoutThreshold, n.rpc.SubscribeToPollingNewHeads) + if err != nil { + lggr.Errorw("Initial subscribe for polling new heads failed", "nodeState", n.getCachedState(), "err", err) + n.declareUnreachable() + return + } - // TODO: will be removed as part of merging effort with BCI-2875 - n.rpc.SetAliveLoopSub(headsSub.sub) + // TODO: will be removed as part of merging effort with BCI-2875 + n.rpc.SetAliveLoopSub(pollingNewHeadsSub.sub) + defer pollingNewHeadsSub.Unsubscribe() + } else { + headsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "heads"), + n.chainCfg.NodeNoNewHeadsThreshold(), n.rpc.SubscribeToHeads) + if err != nil { + lggr.Errorw("Initial subscribe for heads failed", "nodeState", n.getCachedState(), "err", err) + n.declareUnreachable() + return + } - defer headsSub.Unsubscribe() + // TODO: will be removed as part of merging effort with BCI-2875 + n.rpc.SetAliveLoopSub(headsSub.sub) + defer headsSub.Unsubscribe() + } var finalizedHeadsSub headSubscription[HEAD] if n.chainCfg.FinalityTagEnabled() { @@ -122,19 +138,6 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { defer finalizedHeadsSub.Unsubscribe() } - var pollingNewHeadsSub headSubscription[HEAD] - if newHeadsPollInterval > 0 { - pollingNewHeadsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "newHeadsPoll"), - noNewHeadsTimeoutThreshold, n.rpc.SubscribeToPollingNewHeads) - if err != nil { - lggr.Errorw("Failed to subscribe to polling new heads", "err", err) - n.declareUnreachable() - return - } - - defer pollingNewHeadsSub.Unsubscribe() - } - var pollCh <-chan time.Time if pollInterval > 0 { lggr.Debug("Polling enabled") @@ -267,7 +270,6 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { n.declareUnreachable() return } - receivedNewHead := n.onNewHead(lggr, &localHighestChainInfo, newHead) if receivedNewHead && noNewHeadsTimeoutThreshold > 0 { pollingNewHeadsSub.ResetTimer(noNewHeadsTimeoutThreshold) @@ -275,10 +277,10 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { case <-pollingNewHeadsSub.NoNewHeads: // We haven't received a head on the channel for at least the // threshold amount of time, mark it broken - lggr.Errorw(fmt.Sprintf("RPC's new heads with polling state is out of sync; no new heads received for %s", noNewFinalizedBlocksTimeoutThreshold), "latestReceivedBlockNumber", localHighestChainInfo.BlockNumber) + lggr.Errorw(fmt.Sprintf("RPC's polling new heads state is out of sync; no new heads received for %s", noNewHeadsTimeoutThreshold), "latestReceivedBlockNumber", localHighestChainInfo.BlockNumber) if n.poolInfoProvider != nil { if l, _ := n.poolInfoProvider.LatestChainInfo(); l < 2 { - lggr.Criticalf("RPC's new heads with polling state is out of sync; %s %s", msgCannotDisable, msgDegradedState) + lggr.Criticalf("RPC's polling new heads state is out of sync; %s %s", msgCannotDisable, msgDegradedState) // We don't necessarily want to wait the full timeout to check again, we should // check regularly and log noisily in this state pollingNewHeadsSub.ResetTimer(zombieNodeCheckInterval(noNewHeadsTimeoutThreshold)) @@ -288,7 +290,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { n.declareOutOfSync(syncStatusNoNewFinalizedHead) return case <-pollingNewHeadsSub.Errors: - lggr.Errorw("new heads polling subscription was terminated", "err", err, "nodeState", n.getCachedState()) + lggr.Errorw("polling new head subscription was terminated", "err", err, "nodeState", n.getCachedState()) n.declareUnreachable() return } @@ -458,17 +460,33 @@ func (n *node[CHAIN_ID, HEAD, RPC]) outOfSyncLoop(syncIssues syncStatus) { return } + var err error noNewHeadsTimeoutThreshold := n.chainCfg.NodeNoNewHeadsThreshold() - headsSub, err := n.registerNewSubscription(ctx, lggr.With("subscriptionType", "heads"), - noNewHeadsTimeoutThreshold, n.rpc.SubscribeToHeads) - if err != nil { - lggr.Errorw("Failed to subscribe heads on out-of-sync RPC node", "err", err) - n.declareUnreachable() - return - } + var headsSub, pollingNewHeadsSub headSubscription[HEAD] - lggr.Tracew("Successfully subscribed to heads feed on out-of-sync RPC node") - defer headsSub.Unsubscribe() + // if new head based on http polling is enabled, we will skip regular newHead subscription + if n.nodePoolCfg.NewHeadsPollInterval() > 0 { + pollingNewHeadsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "newHeadsPoll"), + noNewHeadsTimeoutThreshold, n.rpc.SubscribeToPollingNewHeads) + if err != nil { + lggr.Errorw("Failed to subscribe to polling new heads on out-of-sync RPC node", "err", err) + n.declareUnreachable() + return + } + + lggr.Tracew("Successfully subscribed to polling new heads feed on out-of-sync RPC node") + defer pollingNewHeadsSub.Unsubscribe() + } else { + headsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "heads"), + noNewHeadsTimeoutThreshold, n.rpc.SubscribeToHeads) + if err != nil { + lggr.Errorw("Failed to subscribe heads on out-of-sync RPC node", "err", err) + n.declareUnreachable() + return + } + lggr.Tracew("Successfully subscribed to heads feed on out-of-sync RPC node") + defer headsSub.Unsubscribe() + } noNewFinalizedBlocksTimeoutThreshold := n.chainCfg.NoNewFinalizedHeadsThreshold() var finalizedHeadsSub headSubscription[HEAD] @@ -484,21 +502,6 @@ func (n *node[CHAIN_ID, HEAD, RPC]) outOfSyncLoop(syncIssues syncStatus) { lggr.Tracew("Successfully subscribed to finalized heads feed on out-of-sync RPC node") defer finalizedHeadsSub.Unsubscribe() } - - var pollingNewHeadsSub headSubscription[HEAD] - if n.nodePoolCfg.NewHeadsPollInterval() > 0 { - pollingNewHeadsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "newHeadsPoll"), - noNewHeadsTimeoutThreshold, n.rpc.SubscribeToPollingNewHeads) - if err != nil { - lggr.Errorw("Failed to subscribe to polling new heads on out-of-sync RPC node", "err", err) - n.declareUnreachable() - return - } - - lggr.Tracew("Successfully subscribed to polling new heads feed on out-of-sync RPC node") - defer pollingNewHeadsSub.Unsubscribe() - } - _, localHighestChainInfo := n.rpc.GetInterceptedChainInfo() for { if syncIssues == syncStatusSynced { diff --git a/common/client/node_lifecycle_test.go b/common/client/node_lifecycle_test.go index 833bccf7f29..1eab78241d1 100644 --- a/common/client/node_lifecycle_test.go +++ b/common/client/node_lifecycle_test.go @@ -90,7 +90,11 @@ func TestUnit_NodeLifecycle_aliveLoop(t *testing.T) { sub := mocks.NewSubscription(t) sub.On("Err").Return((<-chan error)(nil)) sub.On("Unsubscribe").Once() - opts.rpc.On("SubscribeToHeads", mock.Anything).Return(make(<-chan Head), sub, nil) + if opts.config.newHeadsPollInterval > 0 { + opts.rpc.On("SubscribeToPollingNewHeads", mock.Anything).Return(make(<-chan Head), sub, nil) + } else { + opts.rpc.On("SubscribeToHeads", mock.Anything).Return(make(<-chan Head), sub, nil) + } opts.rpc.On("SetAliveLoopSub", sub).Once() return newDialedNode(t, opts) } @@ -317,6 +321,33 @@ func TestUnit_NodeLifecycle_aliveLoop(t *testing.T) { return node.State() == nodeStateUnreachable }) }) + t.Run("when no polling new heads received for threshold, transitions to out of sync", func(t *testing.T) { + t.Parallel() + rpc := newMockNodeClient[types.ID, Head](t) + rpc.On("GetInterceptedChainInfo").Return(ChainInfo{}, ChainInfo{}).Once() + node := newSubscribedNode(t, testNodeOpts{ + config: testNodeConfig{newHeadsPollInterval: tests.TestInterval}, + chainConfig: clientMocks.ChainConfig{ + NoNewHeadsThresholdVal: tests.TestInterval, + }, + rpc: rpc, + }) + defer func() { assert.NoError(t, node.close()) }() + // tries to redial in outOfSync + rpc.On("Dial", mock.Anything).Return(errors.New("failed to dial")).Run(func(_ mock.Arguments) { + assert.Equal(t, nodeStateOutOfSync, node.State()) + }).Once() + // disconnects all on transfer to unreachable or outOfSync + rpc.On("DisconnectAll").Maybe() + // might be called in unreachable loop + rpc.On("Dial", mock.Anything).Return(errors.New("failed to dial")).Maybe() + node.declareAlive() + tests.AssertEventually(t, func() bool { + // right after outOfSync we'll transfer to unreachable due to returned error on Dial + // we check that we were in out of sync state on first Dial call + return node.State() == nodeStateUnreachable + }) + }) t.Run("when no new heads received for threshold but we are the last live node, forcibly stays alive", func(t *testing.T) { t.Parallel() rpc := newMockNodeClient[types.ID, Head](t) @@ -341,6 +372,30 @@ func TestUnit_NodeLifecycle_aliveLoop(t *testing.T) { tests.AssertLogEventually(t, observedLogs, fmt.Sprintf("RPC endpoint detected out of sync; %s %s", msgCannotDisable, msgDegradedState)) assert.Equal(t, nodeStateAlive, node.State()) }) + t.Run("when no polling new heads received for threshold but we are the last live node, forcibly stays alive", func(t *testing.T) { + t.Parallel() + rpc := newMockNodeClient[types.ID, Head](t) + rpc.On("GetInterceptedChainInfo").Return(ChainInfo{}, ChainInfo{}).Once() + lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) + node := newSubscribedNode(t, testNodeOpts{ + config: testNodeConfig{newHeadsPollInterval: tests.TestInterval}, + lggr: lggr, + chainConfig: clientMocks.ChainConfig{ + NoNewHeadsThresholdVal: tests.TestInterval, + }, + rpc: rpc, + }) + defer func() { assert.NoError(t, node.close()) }() + poolInfo := newMockPoolChainInfoProvider(t) + poolInfo.On("LatestChainInfo").Return(1, ChainInfo{ + BlockNumber: 20, + TotalDifficulty: big.NewInt(10), + }).Once() + node.SetPoolChainInfoProvider(poolInfo) + node.declareAlive() + tests.AssertLogEventually(t, observedLogs, fmt.Sprintf("RPC's polling new heads state is out of sync; %s %s", msgCannotDisable, msgDegradedState)) + assert.Equal(t, nodeStateAlive, node.State()) + }) newSub := func(t *testing.T) *mocks.Subscription { sub := mocks.NewSubscription(t) sub.On("Err").Return((<-chan error)(nil)) @@ -374,6 +429,33 @@ func TestUnit_NodeLifecycle_aliveLoop(t *testing.T) { tests.AssertLogEventually(t, observedLogs, "Subscription channel unexpectedly closed") assert.Equal(t, nodeStateUnreachable, node.State()) }) + t.Run("rpc closed polling new head channel", func(t *testing.T) { + t.Parallel() + rpc := newMockNodeClient[types.ID, Head](t) + ch := make(chan Head) + rpc.On("SubscribeToPollingNewHeads", mock.Anything).Run(func(args mock.Arguments) { + close(ch) + }).Return((<-chan Head)(ch), newSub(t), nil).Once() + rpc.On("SetAliveLoopSub", mock.Anything).Once() + rpc.On("GetInterceptedChainInfo").Return(ChainInfo{}, ChainInfo{}).Once() + lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) + node := newDialedNode(t, testNodeOpts{ + lggr: lggr, + config: testNodeConfig{newHeadsPollInterval: tests.TestInterval}, + chainConfig: clientMocks.ChainConfig{ + NoNewHeadsThresholdVal: tests.TestInterval, + }, + rpc: rpc, + }) + defer func() { assert.NoError(t, node.close()) }() + // disconnects all on transfer to unreachable or outOfSync + rpc.On("DisconnectAll").Once() + // might be called in unreachable loop + rpc.On("Dial", mock.Anything).Return(errors.New("failed to dial")).Maybe() + node.declareAlive() + tests.AssertLogEventually(t, observedLogs, "polling new heads subscription channel unexpectedly closed") + assert.Equal(t, nodeStateUnreachable, node.State()) + }) t.Run("If finality tag is not enabled updates finalized block metric using finality depth and latest head", func(t *testing.T) { t.Parallel() rpc := newMockNodeClient[types.ID, Head](t) @@ -644,6 +726,7 @@ func setupRPCForAliveLoop(t *testing.T, rpc *mockNodeClient[types.ID, Head]) { aliveSubscription.On("Unsubscribe").Maybe() rpc.On("SubscribeToHeads", mock.Anything).Return(make(<-chan Head), aliveSubscription, nil).Maybe() rpc.On("SubscribeToFinalizedHeads", mock.Anything).Return(make(<-chan Head), aliveSubscription, nil).Maybe() + rpc.On("SubscribeToPollingNewHeads", mock.Anything).Return(make(<-chan Head), aliveSubscription, nil).Maybe() rpc.On("SetAliveLoopSub", mock.Anything).Maybe() rpc.On("GetInterceptedChainInfo").Return(ChainInfo{}, ChainInfo{}).Maybe() } @@ -914,6 +997,40 @@ func TestUnit_NodeLifecycle_outOfSyncLoop(t *testing.T) { return node.State() == nodeStateAlive }) }) + t.Run("becomes alive if it receives a polling newer head", func(t *testing.T) { + t.Parallel() + rpc := newMockNodeClient[types.ID, Head](t) + nodeChainID := types.RandomID() + lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) + node := newAliveNode(t, testNodeOpts{ + config: testNodeConfig{newHeadsPollInterval: tests.TestInterval}, + rpc: rpc, + chainID: nodeChainID, + lggr: lggr, + }) + defer func() { assert.NoError(t, node.close()) }() + + rpc.On("Dial", mock.Anything).Return(nil).Once() + rpc.On("ChainID", mock.Anything).Return(nodeChainID, nil).Once() + + outOfSyncSubscription := mocks.NewSubscription(t) + outOfSyncSubscription.On("Err").Return((<-chan error)(nil)) + outOfSyncSubscription.On("Unsubscribe").Once() + const highestBlock = 1000 + ch := make(chan Head) + rpc.On("SubscribeToPollingNewHeads", mock.Anything).Run(func(args mock.Arguments) { + go writeHeads(t, ch, head{BlockNumber: highestBlock - 1}, head{BlockNumber: highestBlock}, head{BlockNumber: highestBlock + 1}) + }).Return((<-chan Head)(ch), outOfSyncSubscription, nil).Once() + rpc.On("GetInterceptedChainInfo").Return(ChainInfo{BlockNumber: highestBlock}, ChainInfo{BlockNumber: highestBlock}) + setupRPCForAliveLoop(t, rpc) + + node.declareOutOfSync(syncStatusNoNewHead) + tests.AssertLogEventually(t, observedLogs, msgReceivedPollingBlock) + tests.AssertLogEventually(t, observedLogs, msgInSync) + tests.AssertEventually(t, func() bool { + return node.State() == nodeStateAlive + }) + }) t.Run("becomes alive if there is no other nodes", func(t *testing.T) { t.Parallel() rpc := newMockNodeClient[types.ID, Head](t) From 7280eae31abb6851df1003d2d7708c2fb012c6c1 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Wed, 11 Sep 2024 17:01:43 -0500 Subject: [PATCH 08/27] update changeset --- .changeset/happy-feet-rhyme.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.changeset/happy-feet-rhyme.md b/.changeset/happy-feet-rhyme.md index 011aace101d..9229b6e74e2 100644 --- a/.changeset/happy-feet-rhyme.md +++ b/.changeset/happy-feet-rhyme.md @@ -2,4 +2,10 @@ "chainlink": minor --- -Add a new flag support called `NewHeadsPollInterval`, which is an interval for polling new block periodically using http client rather than subscribe to ws feed. #added +This PR introduce few changes: +- Add a new flag support called `NewHeadsPollInterval` (0 by default indicate disabled), which is an interval for polling new block periodically using http client rather than subscribe to ws feed. +- Created a new handler for polling new head over http, and register the subscription in node lifecycle logic +- If the polling new heads is enabled, WS new heads subscription will be replaced with the new http based polling. + +Note: There will be another PR for making WS URL optional with some extra condition. +#added From 309499ff9583c48b5220cd0ddfefbebb4c7582d9 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Wed, 11 Sep 2024 17:07:15 -0500 Subject: [PATCH 09/27] update comments --- common/client/node_lifecycle.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/client/node_lifecycle.go b/common/client/node_lifecycle.go index 749265579d1..d645b68993e 100644 --- a/common/client/node_lifecycle.go +++ b/common/client/node_lifecycle.go @@ -98,7 +98,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { var err error var headsSub, pollingNewHeadsSub headSubscription[HEAD] - // if new head based on http polling is enabled, we will skip regular newHead subscription + // if new head based on http polling is enabled, we will replace it for WS newHead subscription if newHeadsPollInterval > 0 { pollingNewHeadsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "newHeadsPoll"), noNewHeadsTimeoutThreshold, n.rpc.SubscribeToPollingNewHeads) @@ -464,7 +464,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) outOfSyncLoop(syncIssues syncStatus) { noNewHeadsTimeoutThreshold := n.chainCfg.NodeNoNewHeadsThreshold() var headsSub, pollingNewHeadsSub headSubscription[HEAD] - // if new head based on http polling is enabled, we will skip regular newHead subscription + // if new head based on http polling is enabled, we will replace it for WS newHead subscription if n.nodePoolCfg.NewHeadsPollInterval() > 0 { pollingNewHeadsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "newHeadsPoll"), noNewHeadsTimeoutThreshold, n.rpc.SubscribeToPollingNewHeads) From 70c566606d8f9175b65981078445699c0220b1bc Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 13 Sep 2024 15:11:03 -0500 Subject: [PATCH 10/27] simplify step 1, need to fix test --- .changeset/happy-feet-rhyme.md | 2 +- common/client/mock_node_client_test.go | 67 ---------- common/client/mock_rpc_test.go | 67 ---------- common/client/node_lifecycle.go | 135 +++------------------ common/client/node_lifecycle_test.go | 2 +- common/client/types.go | 1 - core/chains/evm/client/chain_client.go | 5 + core/chains/evm/client/mocks/rpc_client.go | 67 ---------- core/chains/evm/client/rpc_client.go | 27 +++-- 9 files changed, 41 insertions(+), 332 deletions(-) diff --git a/.changeset/happy-feet-rhyme.md b/.changeset/happy-feet-rhyme.md index 9229b6e74e2..be6a32c9ab4 100644 --- a/.changeset/happy-feet-rhyme.md +++ b/.changeset/happy-feet-rhyme.md @@ -4,7 +4,7 @@ This PR introduce few changes: - Add a new flag support called `NewHeadsPollInterval` (0 by default indicate disabled), which is an interval for polling new block periodically using http client rather than subscribe to ws feed. -- Created a new handler for polling new head over http, and register the subscription in node lifecycle logic +- Updated new head handler for polling new head over http, and register the subscription in node lifecycle logic. - If the polling new heads is enabled, WS new heads subscription will be replaced with the new http based polling. Note: There will be another PR for making WS URL optional with some extra condition. diff --git a/common/client/mock_node_client_test.go b/common/client/mock_node_client_test.go index 07ed5cb31da..5643dcde90e 100644 --- a/common/client/mock_node_client_test.go +++ b/common/client/mock_node_client_test.go @@ -626,73 +626,6 @@ func (_c *mockNodeClient_SubscribeToHeads_Call[CHAIN_ID, HEAD]) RunAndReturn(run return _c } -// SubscribeToPollingNewHeads provides a mock function with given fields: _a0 -func (_m *mockNodeClient[CHAIN_ID, HEAD]) SubscribeToPollingNewHeads(_a0 context.Context) (<-chan HEAD, types.Subscription, error) { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for SubscribeToPollingNewHeads") - } - - var r0 <-chan HEAD - var r1 types.Subscription - var r2 error - if rf, ok := ret.Get(0).(func(context.Context) (<-chan HEAD, types.Subscription, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) <-chan HEAD); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(<-chan HEAD) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) types.Subscription); ok { - r1 = rf(_a0) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(types.Subscription) - } - } - - if rf, ok := ret.Get(2).(func(context.Context) error); ok { - r2 = rf(_a0) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// mockNodeClient_SubscribeToPollingNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToPollingNewHeads' -type mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID types.ID, HEAD Head] struct { - *mock.Call -} - -// SubscribeToPollingNewHeads is a helper method to define mock.On call -// - _a0 context.Context -func (_e *mockNodeClient_Expecter[CHAIN_ID, HEAD]) SubscribeToPollingNewHeads(_a0 interface{}) *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD] { - return &mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD]{Call: _e.mock.On("SubscribeToPollingNewHeads", _a0)} -} - -func (_c *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD]) Run(run func(_a0 context.Context)) *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD] { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD]) Return(_a0 <-chan HEAD, _a1 types.Subscription, _a2 error) *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD] { - _c.Call.Return(_a0, _a1, _a2) - return _c -} - -func (_c *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD]) RunAndReturn(run func(context.Context) (<-chan HEAD, types.Subscription, error)) *mockNodeClient_SubscribeToPollingNewHeads_Call[CHAIN_ID, HEAD] { - _c.Call.Return(run) - return _c -} - // SubscribersCount provides a mock function with given fields: func (_m *mockNodeClient[CHAIN_ID, HEAD]) SubscribersCount() int32 { ret := _m.Called() diff --git a/common/client/mock_rpc_test.go b/common/client/mock_rpc_test.go index 46f6b9179a3..9d912b08c2c 100644 --- a/common/client/mock_rpc_test.go +++ b/common/client/mock_rpc_test.go @@ -1708,73 +1708,6 @@ func (_c *mockRPC_SubscribeToHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_ return _c } -// SubscribeToPollingNewHeads provides a mock function with given fields: _a0 -func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) SubscribeToPollingNewHeads(_a0 context.Context) (<-chan HEAD, types.Subscription, error) { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for SubscribeToPollingNewHeads") - } - - var r0 <-chan HEAD - var r1 types.Subscription - var r2 error - if rf, ok := ret.Get(0).(func(context.Context) (<-chan HEAD, types.Subscription, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) <-chan HEAD); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(<-chan HEAD) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) types.Subscription); ok { - r1 = rf(_a0) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(types.Subscription) - } - } - - if rf, ok := ret.Get(2).(func(context.Context) error); ok { - r2 = rf(_a0) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// mockRPC_SubscribeToPollingNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToPollingNewHeads' -type mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID types.ID, SEQ types.Sequence, ADDR types.Hashable, BLOCK_HASH types.Hashable, TX interface{}, TX_HASH types.Hashable, EVENT interface{}, EVENT_OPS interface{}, TX_RECEIPT types.Receipt[TX_HASH, BLOCK_HASH], FEE feetypes.Fee, HEAD types.Head[BLOCK_HASH], BATCH_ELEM interface{}] struct { - *mock.Call -} - -// SubscribeToPollingNewHeads is a helper method to define mock.On call -// - _a0 context.Context -func (_e *mockRPC_Expecter[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) SubscribeToPollingNewHeads(_a0 interface{}) *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { - return &mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]{Call: _e.mock.On("SubscribeToPollingNewHeads", _a0)} -} - -func (_c *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Run(run func(_a0 context.Context)) *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Return(_a0 <-chan HEAD, _a1 types.Subscription, _a2 error) *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { - _c.Call.Return(_a0, _a1, _a2) - return _c -} - -func (_c *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) RunAndReturn(run func(context.Context) (<-chan HEAD, types.Subscription, error)) *mockRPC_SubscribeToPollingNewHeads_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { - _c.Call.Return(run) - return _c -} - // SubscribersCount provides a mock function with given fields: func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) SubscribersCount() int32 { ret := _m.Called() diff --git a/common/client/node_lifecycle.go b/common/client/node_lifecycle.go index d645b68993e..02dbdb31247 100644 --- a/common/client/node_lifecycle.go +++ b/common/client/node_lifecycle.go @@ -91,39 +91,22 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { noNewFinalizedBlocksTimeoutThreshold := n.chainCfg.NoNewFinalizedHeadsThreshold() pollFailureThreshold := n.nodePoolCfg.PollFailureThreshold() pollInterval := n.nodePoolCfg.PollInterval() - newHeadsPollInterval := n.nodePoolCfg.NewHeadsPollInterval() lggr := logger.Sugared(n.lfcLog).Named("Alive").With("noNewHeadsTimeoutThreshold", noNewHeadsTimeoutThreshold, "pollInterval", pollInterval, "pollFailureThreshold", pollFailureThreshold) lggr.Tracew("Alive loop starting", "nodeState", n.getCachedState()) - var err error - var headsSub, pollingNewHeadsSub headSubscription[HEAD] - // if new head based on http polling is enabled, we will replace it for WS newHead subscription - if newHeadsPollInterval > 0 { - pollingNewHeadsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "newHeadsPoll"), - noNewHeadsTimeoutThreshold, n.rpc.SubscribeToPollingNewHeads) - if err != nil { - lggr.Errorw("Initial subscribe for polling new heads failed", "nodeState", n.getCachedState(), "err", err) - n.declareUnreachable() - return - } + headsSub, err := n.registerNewSubscription(ctx, lggr.With("subscriptionType", "heads"), + n.chainCfg.NodeNoNewHeadsThreshold(), n.rpc.SubscribeToHeads) + if err != nil { + lggr.Errorw("Initial subscribe for heads failed", "nodeState", n.getCachedState(), "err", err) + n.declareUnreachable() + return + } - // TODO: will be removed as part of merging effort with BCI-2875 - n.rpc.SetAliveLoopSub(pollingNewHeadsSub.sub) - defer pollingNewHeadsSub.Unsubscribe() - } else { - headsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "heads"), - n.chainCfg.NodeNoNewHeadsThreshold(), n.rpc.SubscribeToHeads) - if err != nil { - lggr.Errorw("Initial subscribe for heads failed", "nodeState", n.getCachedState(), "err", err) - n.declareUnreachable() - return - } + // TODO: will be removed as part of merging effort with BCI-2875 + n.rpc.SetAliveLoopSub(headsSub.sub) - // TODO: will be removed as part of merging effort with BCI-2875 - n.rpc.SetAliveLoopSub(headsSub.sub) - defer headsSub.Unsubscribe() - } + defer headsSub.Unsubscribe() var finalizedHeadsSub headSubscription[HEAD] if n.chainCfg.FinalityTagEnabled() { @@ -264,35 +247,6 @@ func (n *node[CHAIN_ID, HEAD, RPC]) aliveLoop() { lggr.Errorw("Finalized heads subscription was terminated", "err", err) n.declareUnreachable() return - case newHead, open := <-pollingNewHeadsSub.Heads: - if !open { - lggr.Errorw("polling new heads subscription channel unexpectedly closed", "nodeState", n.getCachedState()) - n.declareUnreachable() - return - } - receivedNewHead := n.onNewHead(lggr, &localHighestChainInfo, newHead) - if receivedNewHead && noNewHeadsTimeoutThreshold > 0 { - pollingNewHeadsSub.ResetTimer(noNewHeadsTimeoutThreshold) - } - case <-pollingNewHeadsSub.NoNewHeads: - // We haven't received a head on the channel for at least the - // threshold amount of time, mark it broken - lggr.Errorw(fmt.Sprintf("RPC's polling new heads state is out of sync; no new heads received for %s", noNewHeadsTimeoutThreshold), "latestReceivedBlockNumber", localHighestChainInfo.BlockNumber) - if n.poolInfoProvider != nil { - if l, _ := n.poolInfoProvider.LatestChainInfo(); l < 2 { - lggr.Criticalf("RPC's polling new heads state is out of sync; %s %s", msgCannotDisable, msgDegradedState) - // We don't necessarily want to wait the full timeout to check again, we should - // check regularly and log noisily in this state - pollingNewHeadsSub.ResetTimer(zombieNodeCheckInterval(noNewHeadsTimeoutThreshold)) - continue - } - } - n.declareOutOfSync(syncStatusNoNewFinalizedHead) - return - case <-pollingNewHeadsSub.Errors: - lggr.Errorw("polling new head subscription was terminated", "err", err, "nodeState", n.getCachedState()) - n.declareUnreachable() - return } } } @@ -460,34 +414,18 @@ func (n *node[CHAIN_ID, HEAD, RPC]) outOfSyncLoop(syncIssues syncStatus) { return } - var err error noNewHeadsTimeoutThreshold := n.chainCfg.NodeNoNewHeadsThreshold() - var headsSub, pollingNewHeadsSub headSubscription[HEAD] - - // if new head based on http polling is enabled, we will replace it for WS newHead subscription - if n.nodePoolCfg.NewHeadsPollInterval() > 0 { - pollingNewHeadsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "newHeadsPoll"), - noNewHeadsTimeoutThreshold, n.rpc.SubscribeToPollingNewHeads) - if err != nil { - lggr.Errorw("Failed to subscribe to polling new heads on out-of-sync RPC node", "err", err) - n.declareUnreachable() - return - } - - lggr.Tracew("Successfully subscribed to polling new heads feed on out-of-sync RPC node") - defer pollingNewHeadsSub.Unsubscribe() - } else { - headsSub, err = n.registerNewSubscription(ctx, lggr.With("subscriptionType", "heads"), - noNewHeadsTimeoutThreshold, n.rpc.SubscribeToHeads) - if err != nil { - lggr.Errorw("Failed to subscribe heads on out-of-sync RPC node", "err", err) - n.declareUnreachable() - return - } - lggr.Tracew("Successfully subscribed to heads feed on out-of-sync RPC node") - defer headsSub.Unsubscribe() + headsSub, err := n.registerNewSubscription(ctx, lggr.With("subscriptionType", "heads"), + noNewHeadsTimeoutThreshold, n.rpc.SubscribeToHeads) + if err != nil { + lggr.Errorw("Failed to subscribe heads on out-of-sync RPC node", "err", err) + n.declareUnreachable() + return } + lggr.Tracew("Successfully subscribed to heads feed on out-of-sync RPC node") + defer headsSub.Unsubscribe() + noNewFinalizedBlocksTimeoutThreshold := n.chainCfg.NoNewFinalizedHeadsThreshold() var finalizedHeadsSub headSubscription[HEAD] if n.chainCfg.FinalityTagEnabled() { @@ -502,6 +440,7 @@ func (n *node[CHAIN_ID, HEAD, RPC]) outOfSyncLoop(syncIssues syncStatus) { lggr.Tracew("Successfully subscribed to finalized heads feed on out-of-sync RPC node") defer finalizedHeadsSub.Unsubscribe() } + _, localHighestChainInfo := n.rpc.GetInterceptedChainInfo() for { if syncIssues == syncStatusSynced { @@ -587,40 +526,6 @@ func (n *node[CHAIN_ID, HEAD, RPC]) outOfSyncLoop(syncIssues syncStatus) { // we are not resetting the timer, as there is no need to add syncStatusNoNewFinalizedHead until it's removed on new finalized head. syncIssues |= syncStatusNoNewFinalizedHead lggr.Debugw(fmt.Sprintf("No new finalized heads received for %s. Node stays out-of-sync due to sync issues: %s", noNewFinalizedBlocksTimeoutThreshold, syncIssues)) - case head, open := <-pollingNewHeadsSub.Heads: - if !open { - lggr.Errorw("polling new heads subscription channel unexpectedly closed", "nodeState", n.getCachedState()) - n.declareUnreachable() - return - } - - if !n.onNewHead(lggr, &localHighestChainInfo, head) { - continue - } - - // received a new head - clear NoNewHead flag - syncIssues &= ^syncStatusNoNewHead - if outOfSync, _ := n.isOutOfSyncWithPool(localHighestChainInfo); !outOfSync { - // we caught up with the pool - clear NotInSyncWithPool flag - syncIssues &= ^syncStatusNotInSyncWithPool - } else { - // we've received new head, but lagging behind the pool, add NotInSyncWithPool flag to prevent false transition to alive - syncIssues |= syncStatusNotInSyncWithPool - } - - if noNewHeadsTimeoutThreshold > 0 { - pollingNewHeadsSub.ResetTimer(noNewHeadsTimeoutThreshold) - } - - lggr.Debugw(msgReceivedPollingBlock, "blockNumber", head.BlockNumber(), "blockDifficulty", head.BlockDifficulty(), "syncIssues", syncIssues) - case err = <-pollingNewHeadsSub.Errors: - lggr.Errorw("polling new heads subscription was terminated", "err", err) - n.declareUnreachable() - return - case <-pollingNewHeadsSub.NoNewHeads: - // we are not resetting the timer, as there is no need to add syncStatusNoNewHead until it's removed on new head. - syncIssues |= syncStatusNoNewHead - lggr.Debugw(fmt.Sprintf("No polling new heads received for %s. Node stays out-of-sync due to sync issues: %s", noNewHeadsTimeoutThreshold, syncIssues)) } } } diff --git a/common/client/node_lifecycle_test.go b/common/client/node_lifecycle_test.go index 1eab78241d1..d395a3dfda4 100644 --- a/common/client/node_lifecycle_test.go +++ b/common/client/node_lifecycle_test.go @@ -1025,7 +1025,7 @@ func TestUnit_NodeLifecycle_outOfSyncLoop(t *testing.T) { setupRPCForAliveLoop(t, rpc) node.declareOutOfSync(syncStatusNoNewHead) - tests.AssertLogEventually(t, observedLogs, msgReceivedPollingBlock) + tests.AssertLogEventually(t, observedLogs, msgReceivedBlock) tests.AssertLogEventually(t, observedLogs, msgInSync) tests.AssertEventually(t, func() bool { return node.State() == nodeStateAlive diff --git a/common/client/types.go b/common/client/types.go index 8f6dd53ea76..197e93ee25c 100644 --- a/common/client/types.go +++ b/common/client/types.go @@ -80,7 +80,6 @@ type NodeClient[ // Ensure implementation does not have a race condition when values are reset before request completion and as // a result latest ChainInfo contains information from the previous cycle. GetInterceptedChainInfo() (latest, highestUserObservations ChainInfo) - SubscribeToPollingNewHeads(_ context.Context) (<-chan HEAD, types.Subscription, error) } // clientAPI includes all the direct RPC methods required by the generalized common client to implement its own. diff --git a/core/chains/evm/client/chain_client.go b/core/chains/evm/client/chain_client.go index 310528424d6..295b323ad7d 100644 --- a/core/chains/evm/client/chain_client.go +++ b/core/chains/evm/client/chain_client.go @@ -61,6 +61,7 @@ type Client interface { // CAUTION: Using this method might cause local finality violations. It's highly recommended // to use HeadTracker to get latest finalized block. LatestFinalizedBlock(ctx context.Context) (head *evmtypes.Head, err error) + LatestBlock(ctx context.Context) (*evmtypes.Head, error) SendTransactionReturnCode(ctx context.Context, tx *types.Transaction, fromAddress common.Address) (commonclient.SendTxReturnCode, error) @@ -354,6 +355,10 @@ func (c *chainClient) LatestFinalizedBlock(ctx context.Context) (*evmtypes.Head, return c.multiNode.LatestFinalizedBlock(ctx) } +func (c *chainClient) LatestBlock(ctx context.Context) (*evmtypes.Head, error) { + return c.multiNode.LatestBlock(ctx) +} + func (c *chainClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { rpc, err := c.multiNode.SelectNodeRPC() if err != nil { diff --git a/core/chains/evm/client/mocks/rpc_client.go b/core/chains/evm/client/mocks/rpc_client.go index 4931274baed..6cddb70b7da 100644 --- a/core/chains/evm/client/mocks/rpc_client.go +++ b/core/chains/evm/client/mocks/rpc_client.go @@ -2145,73 +2145,6 @@ func (_c *RPCClient_SubscribeToHeads_Call) RunAndReturn(run func(context.Context return _c } -// SubscribeToPollingNewHeads provides a mock function with given fields: _a0 -func (_m *RPCClient) SubscribeToPollingNewHeads(_a0 context.Context) (<-chan *types.Head, commontypes.Subscription, error) { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for SubscribeToPollingNewHeads") - } - - var r0 <-chan *types.Head - var r1 commontypes.Subscription - var r2 error - if rf, ok := ret.Get(0).(func(context.Context) (<-chan *types.Head, commontypes.Subscription, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) <-chan *types.Head); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(<-chan *types.Head) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) commontypes.Subscription); ok { - r1 = rf(_a0) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(commontypes.Subscription) - } - } - - if rf, ok := ret.Get(2).(func(context.Context) error); ok { - r2 = rf(_a0) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// RPCClient_SubscribeToPollingNewHeads_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeToPollingNewHeads' -type RPCClient_SubscribeToPollingNewHeads_Call struct { - *mock.Call -} - -// SubscribeToPollingNewHeads is a helper method to define mock.On call -// - _a0 context.Context -func (_e *RPCClient_Expecter) SubscribeToPollingNewHeads(_a0 interface{}) *RPCClient_SubscribeToPollingNewHeads_Call { - return &RPCClient_SubscribeToPollingNewHeads_Call{Call: _e.mock.On("SubscribeToPollingNewHeads", _a0)} -} - -func (_c *RPCClient_SubscribeToPollingNewHeads_Call) Run(run func(_a0 context.Context)) *RPCClient_SubscribeToPollingNewHeads_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *RPCClient_SubscribeToPollingNewHeads_Call) Return(_a0 <-chan *types.Head, _a1 commontypes.Subscription, _a2 error) *RPCClient_SubscribeToPollingNewHeads_Call { - _c.Call.Return(_a0, _a1, _a2) - return _c -} - -func (_c *RPCClient_SubscribeToPollingNewHeads_Call) RunAndReturn(run func(context.Context) (<-chan *types.Head, commontypes.Subscription, error)) *RPCClient_SubscribeToPollingNewHeads_Call { - _c.Call.Return(run) - return _c -} - // SubscribersCount provides a mock function with given fields: func (_m *RPCClient) SubscribersCount() int32 { ret := _m.Called() diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 8fcecc7f98b..1c29b022b82 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -526,6 +526,20 @@ func (r *rpcClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.H start := time.Now() lggr := r.newRqLggr().With("args", args) + // if new head based on http polling is enabled, we will replace it for WS newHead subscription + if r.newHeadsPollInterval > 0 { + interval := r.newHeadsPollInterval + if interval == 0 { + return nil, nil, errors.New("NewHeadsPollInterval is 0") + } + timeout := interval + poller, channel := commonclient.NewPoller[*evmtypes.Head](interval, r.LatestBlock, timeout, r.rpcLog) + if err := poller.Start(ctx); err != nil { + return nil, nil, err + } + return channel, &poller, nil + } + lggr.Debug("RPC call: evmclient.Client#EthSubscribe") defer func() { duration := time.Since(start) @@ -566,19 +580,6 @@ func (r *rpcClient) SubscribeToFinalizedHeads(ctx context.Context) (<-chan *evmt return channel, &poller, nil } -func (r *rpcClient) SubscribeToPollingNewHeads(ctx context.Context) (<-chan *evmtypes.Head, commontypes.Subscription, error) { - interval := r.newHeadsPollInterval - if interval == 0 { - return nil, nil, errors.New("NewHeadsPollInterval is 0") - } - timeout := interval - poller, channel := commonclient.NewPoller[*evmtypes.Head](interval, r.LatestBlock, timeout, r.rpcLog) - if err := poller.Start(ctx); err != nil { - return nil, nil, err - } - return channel, &poller, nil -} - // GethClient wrappers func (r *rpcClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (receipt *evmtypes.Receipt, err error) { From b819f89cebc55d062496f29873b47264305d3e0e Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 13 Sep 2024 15:38:02 -0500 Subject: [PATCH 11/27] remove tests, no longer needed --- common/client/node_lifecycle.go | 1 - common/client/node_lifecycle_test.go | 119 +-------------------------- core/chains/evm/client/rpc_client.go | 7 +- 3 files changed, 4 insertions(+), 123 deletions(-) diff --git a/common/client/node_lifecycle.go b/common/client/node_lifecycle.go index 02dbdb31247..40d9a9ef6ef 100644 --- a/common/client/node_lifecycle.go +++ b/common/client/node_lifecycle.go @@ -379,7 +379,6 @@ func (n *node[CHAIN_ID, HEAD, RPC]) isOutOfSyncWithPool(localState ChainInfo) (o const ( msgReceivedBlock = "Received block for RPC node, waiting until back in-sync to mark as live again" msgReceivedFinalizedBlock = "Received new finalized block for RPC node, waiting until back in-sync to mark as live again" - msgReceivedPollingBlock = "Received polling block for RPC node, waiting until back in-sync to mark as live again" msgInSync = "RPC node back in sync" ) diff --git a/common/client/node_lifecycle_test.go b/common/client/node_lifecycle_test.go index d395a3dfda4..833bccf7f29 100644 --- a/common/client/node_lifecycle_test.go +++ b/common/client/node_lifecycle_test.go @@ -90,11 +90,7 @@ func TestUnit_NodeLifecycle_aliveLoop(t *testing.T) { sub := mocks.NewSubscription(t) sub.On("Err").Return((<-chan error)(nil)) sub.On("Unsubscribe").Once() - if opts.config.newHeadsPollInterval > 0 { - opts.rpc.On("SubscribeToPollingNewHeads", mock.Anything).Return(make(<-chan Head), sub, nil) - } else { - opts.rpc.On("SubscribeToHeads", mock.Anything).Return(make(<-chan Head), sub, nil) - } + opts.rpc.On("SubscribeToHeads", mock.Anything).Return(make(<-chan Head), sub, nil) opts.rpc.On("SetAliveLoopSub", sub).Once() return newDialedNode(t, opts) } @@ -321,33 +317,6 @@ func TestUnit_NodeLifecycle_aliveLoop(t *testing.T) { return node.State() == nodeStateUnreachable }) }) - t.Run("when no polling new heads received for threshold, transitions to out of sync", func(t *testing.T) { - t.Parallel() - rpc := newMockNodeClient[types.ID, Head](t) - rpc.On("GetInterceptedChainInfo").Return(ChainInfo{}, ChainInfo{}).Once() - node := newSubscribedNode(t, testNodeOpts{ - config: testNodeConfig{newHeadsPollInterval: tests.TestInterval}, - chainConfig: clientMocks.ChainConfig{ - NoNewHeadsThresholdVal: tests.TestInterval, - }, - rpc: rpc, - }) - defer func() { assert.NoError(t, node.close()) }() - // tries to redial in outOfSync - rpc.On("Dial", mock.Anything).Return(errors.New("failed to dial")).Run(func(_ mock.Arguments) { - assert.Equal(t, nodeStateOutOfSync, node.State()) - }).Once() - // disconnects all on transfer to unreachable or outOfSync - rpc.On("DisconnectAll").Maybe() - // might be called in unreachable loop - rpc.On("Dial", mock.Anything).Return(errors.New("failed to dial")).Maybe() - node.declareAlive() - tests.AssertEventually(t, func() bool { - // right after outOfSync we'll transfer to unreachable due to returned error on Dial - // we check that we were in out of sync state on first Dial call - return node.State() == nodeStateUnreachable - }) - }) t.Run("when no new heads received for threshold but we are the last live node, forcibly stays alive", func(t *testing.T) { t.Parallel() rpc := newMockNodeClient[types.ID, Head](t) @@ -372,30 +341,6 @@ func TestUnit_NodeLifecycle_aliveLoop(t *testing.T) { tests.AssertLogEventually(t, observedLogs, fmt.Sprintf("RPC endpoint detected out of sync; %s %s", msgCannotDisable, msgDegradedState)) assert.Equal(t, nodeStateAlive, node.State()) }) - t.Run("when no polling new heads received for threshold but we are the last live node, forcibly stays alive", func(t *testing.T) { - t.Parallel() - rpc := newMockNodeClient[types.ID, Head](t) - rpc.On("GetInterceptedChainInfo").Return(ChainInfo{}, ChainInfo{}).Once() - lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - node := newSubscribedNode(t, testNodeOpts{ - config: testNodeConfig{newHeadsPollInterval: tests.TestInterval}, - lggr: lggr, - chainConfig: clientMocks.ChainConfig{ - NoNewHeadsThresholdVal: tests.TestInterval, - }, - rpc: rpc, - }) - defer func() { assert.NoError(t, node.close()) }() - poolInfo := newMockPoolChainInfoProvider(t) - poolInfo.On("LatestChainInfo").Return(1, ChainInfo{ - BlockNumber: 20, - TotalDifficulty: big.NewInt(10), - }).Once() - node.SetPoolChainInfoProvider(poolInfo) - node.declareAlive() - tests.AssertLogEventually(t, observedLogs, fmt.Sprintf("RPC's polling new heads state is out of sync; %s %s", msgCannotDisable, msgDegradedState)) - assert.Equal(t, nodeStateAlive, node.State()) - }) newSub := func(t *testing.T) *mocks.Subscription { sub := mocks.NewSubscription(t) sub.On("Err").Return((<-chan error)(nil)) @@ -429,33 +374,6 @@ func TestUnit_NodeLifecycle_aliveLoop(t *testing.T) { tests.AssertLogEventually(t, observedLogs, "Subscription channel unexpectedly closed") assert.Equal(t, nodeStateUnreachable, node.State()) }) - t.Run("rpc closed polling new head channel", func(t *testing.T) { - t.Parallel() - rpc := newMockNodeClient[types.ID, Head](t) - ch := make(chan Head) - rpc.On("SubscribeToPollingNewHeads", mock.Anything).Run(func(args mock.Arguments) { - close(ch) - }).Return((<-chan Head)(ch), newSub(t), nil).Once() - rpc.On("SetAliveLoopSub", mock.Anything).Once() - rpc.On("GetInterceptedChainInfo").Return(ChainInfo{}, ChainInfo{}).Once() - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - node := newDialedNode(t, testNodeOpts{ - lggr: lggr, - config: testNodeConfig{newHeadsPollInterval: tests.TestInterval}, - chainConfig: clientMocks.ChainConfig{ - NoNewHeadsThresholdVal: tests.TestInterval, - }, - rpc: rpc, - }) - defer func() { assert.NoError(t, node.close()) }() - // disconnects all on transfer to unreachable or outOfSync - rpc.On("DisconnectAll").Once() - // might be called in unreachable loop - rpc.On("Dial", mock.Anything).Return(errors.New("failed to dial")).Maybe() - node.declareAlive() - tests.AssertLogEventually(t, observedLogs, "polling new heads subscription channel unexpectedly closed") - assert.Equal(t, nodeStateUnreachable, node.State()) - }) t.Run("If finality tag is not enabled updates finalized block metric using finality depth and latest head", func(t *testing.T) { t.Parallel() rpc := newMockNodeClient[types.ID, Head](t) @@ -726,7 +644,6 @@ func setupRPCForAliveLoop(t *testing.T, rpc *mockNodeClient[types.ID, Head]) { aliveSubscription.On("Unsubscribe").Maybe() rpc.On("SubscribeToHeads", mock.Anything).Return(make(<-chan Head), aliveSubscription, nil).Maybe() rpc.On("SubscribeToFinalizedHeads", mock.Anything).Return(make(<-chan Head), aliveSubscription, nil).Maybe() - rpc.On("SubscribeToPollingNewHeads", mock.Anything).Return(make(<-chan Head), aliveSubscription, nil).Maybe() rpc.On("SetAliveLoopSub", mock.Anything).Maybe() rpc.On("GetInterceptedChainInfo").Return(ChainInfo{}, ChainInfo{}).Maybe() } @@ -997,40 +914,6 @@ func TestUnit_NodeLifecycle_outOfSyncLoop(t *testing.T) { return node.State() == nodeStateAlive }) }) - t.Run("becomes alive if it receives a polling newer head", func(t *testing.T) { - t.Parallel() - rpc := newMockNodeClient[types.ID, Head](t) - nodeChainID := types.RandomID() - lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - node := newAliveNode(t, testNodeOpts{ - config: testNodeConfig{newHeadsPollInterval: tests.TestInterval}, - rpc: rpc, - chainID: nodeChainID, - lggr: lggr, - }) - defer func() { assert.NoError(t, node.close()) }() - - rpc.On("Dial", mock.Anything).Return(nil).Once() - rpc.On("ChainID", mock.Anything).Return(nodeChainID, nil).Once() - - outOfSyncSubscription := mocks.NewSubscription(t) - outOfSyncSubscription.On("Err").Return((<-chan error)(nil)) - outOfSyncSubscription.On("Unsubscribe").Once() - const highestBlock = 1000 - ch := make(chan Head) - rpc.On("SubscribeToPollingNewHeads", mock.Anything).Run(func(args mock.Arguments) { - go writeHeads(t, ch, head{BlockNumber: highestBlock - 1}, head{BlockNumber: highestBlock}, head{BlockNumber: highestBlock + 1}) - }).Return((<-chan Head)(ch), outOfSyncSubscription, nil).Once() - rpc.On("GetInterceptedChainInfo").Return(ChainInfo{BlockNumber: highestBlock}, ChainInfo{BlockNumber: highestBlock}) - setupRPCForAliveLoop(t, rpc) - - node.declareOutOfSync(syncStatusNoNewHead) - tests.AssertLogEventually(t, observedLogs, msgReceivedBlock) - tests.AssertLogEventually(t, observedLogs, msgInSync) - tests.AssertEventually(t, func() bool { - return node.State() == nodeStateAlive - }) - }) t.Run("becomes alive if there is no other nodes", func(t *testing.T) { t.Parallel() rpc := newMockNodeClient[types.ID, Head](t) diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 1c29b022b82..df720bebcf2 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -529,14 +529,13 @@ func (r *rpcClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.H // if new head based on http polling is enabled, we will replace it for WS newHead subscription if r.newHeadsPollInterval > 0 { interval := r.newHeadsPollInterval - if interval == 0 { - return nil, nil, errors.New("NewHeadsPollInterval is 0") - } timeout := interval poller, channel := commonclient.NewPoller[*evmtypes.Head](interval, r.LatestBlock, timeout, r.rpcLog) - if err := poller.Start(ctx); err != nil { + if err = poller.Start(ctx); err != nil { return nil, nil, err } + + lggr.Debugf("Polling new heads over http ") return channel, &poller, nil } From 4e8e35f528a3cc046ffe38720df654dc565565e2 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 13 Sep 2024 15:43:30 -0500 Subject: [PATCH 12/27] fix lint --- core/chains/evm/client/null_client.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/chains/evm/client/null_client.go b/core/chains/evm/client/null_client.go index 5b1a4d7e1bb..78d3a99680e 100644 --- a/core/chains/evm/client/null_client.go +++ b/core/chains/evm/client/null_client.go @@ -239,3 +239,7 @@ func (nc *NullClient) CheckTxValidity(_ context.Context, _ common.Address, _ com func (nc *NullClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { return nil, nil } + +func (nc *NullClient) LatestBlock(ctx context.Context) (*evmtypes.Head, error) { + return nil, nil +} From 22f90f01161d7c238c20464532b2c71b88e39cec Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 13 Sep 2024 15:59:05 -0500 Subject: [PATCH 13/27] fix mock --- core/chains/evm/client/mocks/client.go | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/core/chains/evm/client/mocks/client.go b/core/chains/evm/client/mocks/client.go index da034d95774..d0a14ceb43c 100644 --- a/core/chains/evm/client/mocks/client.go +++ b/core/chains/evm/client/mocks/client.go @@ -1240,6 +1240,64 @@ func (_c *Client_LINKBalance_Call) RunAndReturn(run func(context.Context, common return _c } +// LatestBlock provides a mock function with given fields: ctx +func (_m *Client) LatestBlock(ctx context.Context) (*evmtypes.Head, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for LatestBlock") + } + + var r0 *evmtypes.Head + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*evmtypes.Head, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *evmtypes.Head); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*evmtypes.Head) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_LatestBlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestBlock' +type Client_LatestBlock_Call struct { + *mock.Call +} + +// LatestBlock is a helper method to define mock.On call +// - ctx context.Context +func (_e *Client_Expecter) LatestBlock(ctx interface{}) *Client_LatestBlock_Call { + return &Client_LatestBlock_Call{Call: _e.mock.On("LatestBlock", ctx)} +} + +func (_c *Client_LatestBlock_Call) Run(run func(ctx context.Context)) *Client_LatestBlock_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Client_LatestBlock_Call) Return(_a0 *evmtypes.Head, _a1 error) *Client_LatestBlock_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_LatestBlock_Call) RunAndReturn(run func(context.Context) (*evmtypes.Head, error)) *Client_LatestBlock_Call { + _c.Call.Return(run) + return _c +} + // LatestBlockHeight provides a mock function with given fields: ctx func (_m *Client) LatestBlockHeight(ctx context.Context) (*big.Int, error) { ret := _m.Called(ctx) From 87f3148663a814e75a72bfaffa2c3410225a3640 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 13 Sep 2024 16:10:46 -0500 Subject: [PATCH 14/27] fix simulated client --- core/chains/evm/client/simulated_backend_client.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/chains/evm/client/simulated_backend_client.go b/core/chains/evm/client/simulated_backend_client.go index 6c569b16b85..0463f56cbdc 100644 --- a/core/chains/evm/client/simulated_backend_client.go +++ b/core/chains/evm/client/simulated_backend_client.go @@ -717,6 +717,17 @@ func (c *SimulatedBackendClient) LatestFinalizedBlock(ctx context.Context) (*evm }, nil } +func (c *SimulatedBackendClient) LatestBlock(ctx context.Context) (*evmtypes.Head, error) { + block := c.b.Blockchain().CurrentBlock() + return &evmtypes.Head{ + EVMChainID: ubig.NewI(c.chainId.Int64()), + Hash: block.Hash(), + Number: block.Number.Int64(), + ParentHash: block.ParentHash, + Timestamp: time.Unix(int64(block.Time), 0), + }, nil +} + func (c *SimulatedBackendClient) ethGetLogs(ctx context.Context, result interface{}, args ...interface{}) error { var from, to *big.Int var hash *common.Hash From 9f60dc83421cf6491adf51a38fa39645a4f70e78 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 13 Sep 2024 17:25:10 -0500 Subject: [PATCH 15/27] update interface --- core/chains/evm/client/chain_client.go | 5 -- core/chains/evm/client/mocks/client.go | 58 ------------------- core/chains/evm/client/null_client.go | 4 -- .../evm/client/simulated_backend_client.go | 11 ---- 4 files changed, 78 deletions(-) diff --git a/core/chains/evm/client/chain_client.go b/core/chains/evm/client/chain_client.go index 295b323ad7d..310528424d6 100644 --- a/core/chains/evm/client/chain_client.go +++ b/core/chains/evm/client/chain_client.go @@ -61,7 +61,6 @@ type Client interface { // CAUTION: Using this method might cause local finality violations. It's highly recommended // to use HeadTracker to get latest finalized block. LatestFinalizedBlock(ctx context.Context) (head *evmtypes.Head, err error) - LatestBlock(ctx context.Context) (*evmtypes.Head, error) SendTransactionReturnCode(ctx context.Context, tx *types.Transaction, fromAddress common.Address) (commonclient.SendTxReturnCode, error) @@ -355,10 +354,6 @@ func (c *chainClient) LatestFinalizedBlock(ctx context.Context) (*evmtypes.Head, return c.multiNode.LatestFinalizedBlock(ctx) } -func (c *chainClient) LatestBlock(ctx context.Context) (*evmtypes.Head, error) { - return c.multiNode.LatestBlock(ctx) -} - func (c *chainClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { rpc, err := c.multiNode.SelectNodeRPC() if err != nil { diff --git a/core/chains/evm/client/mocks/client.go b/core/chains/evm/client/mocks/client.go index d0a14ceb43c..da034d95774 100644 --- a/core/chains/evm/client/mocks/client.go +++ b/core/chains/evm/client/mocks/client.go @@ -1240,64 +1240,6 @@ func (_c *Client_LINKBalance_Call) RunAndReturn(run func(context.Context, common return _c } -// LatestBlock provides a mock function with given fields: ctx -func (_m *Client) LatestBlock(ctx context.Context) (*evmtypes.Head, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for LatestBlock") - } - - var r0 *evmtypes.Head - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*evmtypes.Head, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) *evmtypes.Head); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evmtypes.Head) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Client_LatestBlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestBlock' -type Client_LatestBlock_Call struct { - *mock.Call -} - -// LatestBlock is a helper method to define mock.On call -// - ctx context.Context -func (_e *Client_Expecter) LatestBlock(ctx interface{}) *Client_LatestBlock_Call { - return &Client_LatestBlock_Call{Call: _e.mock.On("LatestBlock", ctx)} -} - -func (_c *Client_LatestBlock_Call) Run(run func(ctx context.Context)) *Client_LatestBlock_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *Client_LatestBlock_Call) Return(_a0 *evmtypes.Head, _a1 error) *Client_LatestBlock_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *Client_LatestBlock_Call) RunAndReturn(run func(context.Context) (*evmtypes.Head, error)) *Client_LatestBlock_Call { - _c.Call.Return(run) - return _c -} - // LatestBlockHeight provides a mock function with given fields: ctx func (_m *Client) LatestBlockHeight(ctx context.Context) (*big.Int, error) { ret := _m.Called(ctx) diff --git a/core/chains/evm/client/null_client.go b/core/chains/evm/client/null_client.go index 78d3a99680e..5b1a4d7e1bb 100644 --- a/core/chains/evm/client/null_client.go +++ b/core/chains/evm/client/null_client.go @@ -239,7 +239,3 @@ func (nc *NullClient) CheckTxValidity(_ context.Context, _ common.Address, _ com func (nc *NullClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { return nil, nil } - -func (nc *NullClient) LatestBlock(ctx context.Context) (*evmtypes.Head, error) { - return nil, nil -} diff --git a/core/chains/evm/client/simulated_backend_client.go b/core/chains/evm/client/simulated_backend_client.go index 0463f56cbdc..6c569b16b85 100644 --- a/core/chains/evm/client/simulated_backend_client.go +++ b/core/chains/evm/client/simulated_backend_client.go @@ -717,17 +717,6 @@ func (c *SimulatedBackendClient) LatestFinalizedBlock(ctx context.Context) (*evm }, nil } -func (c *SimulatedBackendClient) LatestBlock(ctx context.Context) (*evmtypes.Head, error) { - block := c.b.Blockchain().CurrentBlock() - return &evmtypes.Head{ - EVMChainID: ubig.NewI(c.chainId.Int64()), - Hash: block.Hash(), - Number: block.Number.Int64(), - ParentHash: block.ParentHash, - Timestamp: time.Unix(int64(block.Time), 0), - }, nil -} - func (c *SimulatedBackendClient) ethGetLogs(ctx context.Context, result interface{}, args ...interface{}) error { var from, to *big.Int var hash *common.Hash From aa4a030527bb1a58720d61e93418229f86142fac Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 13 Sep 2024 17:30:59 -0500 Subject: [PATCH 16/27] rm more --- common/client/mock_rpc_test.go | 56 --------------------- common/client/multi_node.go | 9 ---- common/client/types.go | 1 - core/chains/evm/client/mocks/rpc_client.go | 58 ---------------------- 4 files changed, 124 deletions(-) diff --git a/common/client/mock_rpc_test.go b/common/client/mock_rpc_test.go index 9d912b08c2c..00473c66369 100644 --- a/common/client/mock_rpc_test.go +++ b/common/client/mock_rpc_test.go @@ -983,62 +983,6 @@ func (_c *mockRPC_LINKBalance_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, return _c } -// LatestBlock provides a mock function with given fields: _a0 -func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) LatestBlock(_a0 context.Context) (HEAD, error) { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for LatestBlock") - } - - var r0 HEAD - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (HEAD, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) HEAD); ok { - r0 = rf(_a0) - } else { - r0 = ret.Get(0).(HEAD) - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockRPC_LatestBlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestBlock' -type mockRPC_LatestBlock_Call[CHAIN_ID types.ID, SEQ types.Sequence, ADDR types.Hashable, BLOCK_HASH types.Hashable, TX interface{}, TX_HASH types.Hashable, EVENT interface{}, EVENT_OPS interface{}, TX_RECEIPT types.Receipt[TX_HASH, BLOCK_HASH], FEE feetypes.Fee, HEAD types.Head[BLOCK_HASH], BATCH_ELEM interface{}] struct { - *mock.Call -} - -// LatestBlock is a helper method to define mock.On call -// - _a0 context.Context -func (_e *mockRPC_Expecter[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) LatestBlock(_a0 interface{}) *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { - return &mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]{Call: _e.mock.On("LatestBlock", _a0)} -} - -func (_c *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Run(run func(_a0 context.Context)) *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) Return(_a0 HEAD, _a1 error) *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) RunAndReturn(run func(context.Context) (HEAD, error)) *mockRPC_LatestBlock_Call[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM] { - _c.Call.Return(run) - return _c -} - // LatestBlockHeight provides a mock function with given fields: _a0 func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, BATCH_ELEM]) LatestBlockHeight(_a0 context.Context) (*big.Int, error) { ret := _m.Called(_a0) diff --git a/common/client/multi_node.go b/common/client/multi_node.go index 2a5891e3726..c9250a1d620 100644 --- a/common/client/multi_node.go +++ b/common/client/multi_node.go @@ -862,12 +862,3 @@ func (c *multiNode[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OP return n.RPC().LatestFinalizedBlock(ctx) } - -func (c *multiNode[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, RPC_CLIENT, BATCH_ELEM]) LatestBlock(ctx context.Context) (head HEAD, err error) { - n, err := c.selectNode() - if err != nil { - return head, err - } - - return n.RPC().LatestBlock(ctx) -} diff --git a/common/client/types.go b/common/client/types.go index 197e93ee25c..c9b6a3580eb 100644 --- a/common/client/types.go +++ b/common/client/types.go @@ -126,7 +126,6 @@ type clientAPI[ BlockByHash(ctx context.Context, hash BLOCK_HASH) (HEAD, error) LatestBlockHeight(context.Context) (*big.Int, error) LatestFinalizedBlock(ctx context.Context) (HEAD, error) - LatestBlock(context.Context) (HEAD, error) // Events FilterEvents(ctx context.Context, query EVENT_OPS) ([]EVENT, error) diff --git a/core/chains/evm/client/mocks/rpc_client.go b/core/chains/evm/client/mocks/rpc_client.go index 6cddb70b7da..5567b3f8978 100644 --- a/core/chains/evm/client/mocks/rpc_client.go +++ b/core/chains/evm/client/mocks/rpc_client.go @@ -1297,64 +1297,6 @@ func (_c *RPCClient_LINKBalance_Call) RunAndReturn(run func(context.Context, com return _c } -// LatestBlock provides a mock function with given fields: _a0 -func (_m *RPCClient) LatestBlock(_a0 context.Context) (*types.Head, error) { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for LatestBlock") - } - - var r0 *types.Head - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*types.Head, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) *types.Head); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Head) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RPCClient_LatestBlock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestBlock' -type RPCClient_LatestBlock_Call struct { - *mock.Call -} - -// LatestBlock is a helper method to define mock.On call -// - _a0 context.Context -func (_e *RPCClient_Expecter) LatestBlock(_a0 interface{}) *RPCClient_LatestBlock_Call { - return &RPCClient_LatestBlock_Call{Call: _e.mock.On("LatestBlock", _a0)} -} - -func (_c *RPCClient_LatestBlock_Call) Run(run func(_a0 context.Context)) *RPCClient_LatestBlock_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *RPCClient_LatestBlock_Call) Return(_a0 *types.Head, _a1 error) *RPCClient_LatestBlock_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *RPCClient_LatestBlock_Call) RunAndReturn(run func(context.Context) (*types.Head, error)) *RPCClient_LatestBlock_Call { - _c.Call.Return(run) - return _c -} - // LatestBlockHeight provides a mock function with given fields: _a0 func (_m *RPCClient) LatestBlockHeight(_a0 context.Context) (*big.Int, error) { ret := _m.Called(_a0) From 5d22045951b69934aee10a497ca0c5de0d896676 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Mon, 16 Sep 2024 18:05:54 -0500 Subject: [PATCH 17/27] temp update for testing log level --- core/chains/evm/client/rpc_client.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index df720bebcf2..cea713e9d99 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -527,6 +527,7 @@ func (r *rpcClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.H lggr := r.newRqLggr().With("args", args) // if new head based on http polling is enabled, we will replace it for WS newHead subscription + lggr.Infof("the newHeadsPollInterval is %v", r.newHeadsPollInterval.String()) if r.newHeadsPollInterval > 0 { interval := r.newHeadsPollInterval timeout := interval @@ -535,7 +536,7 @@ func (r *rpcClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.H return nil, nil, err } - lggr.Debugf("Polling new heads over http ") + lggr.Infof("Polling new heads over http ") return channel, &poller, nil } From c98f85044001e10255118d0df73b35fe724ae722 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Mon, 16 Sep 2024 18:08:42 -0500 Subject: [PATCH 18/27] temp update --- core/chains/evm/config/toml/defaults/Polygon_Amoy.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml b/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml index bca42d9b403..a9d148aa52e 100644 --- a/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml +++ b/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml @@ -26,3 +26,4 @@ HistoryDepth = 2000 [NodePool] SyncThreshold = 10 +NewHeadsPollInterval = '1s' From ec63fca31a1fda52624a9e83bd7c652eaa615297 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Tue, 17 Sep 2024 16:50:31 -0500 Subject: [PATCH 19/27] revert modification to polygon toml --- core/chains/evm/config/toml/defaults/Polygon_Amoy.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml b/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml index a9d148aa52e..bca42d9b403 100644 --- a/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml +++ b/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml @@ -26,4 +26,3 @@ HistoryDepth = 2000 [NodePool] SyncThreshold = 10 -NewHeadsPollInterval = '1s' From f0d25ce13dc1c89ad22248a329e36c76e71816d6 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Wed, 18 Sep 2024 10:55:05 -0500 Subject: [PATCH 20/27] enable heap monitoring --- core/chains/evm/config/toml/defaults/fallback.toml | 2 +- core/web/router.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/chains/evm/config/toml/defaults/fallback.toml b/core/chains/evm/config/toml/defaults/fallback.toml index 6f43f956faf..c27d056431e 100644 --- a/core/chains/evm/config/toml/defaults/fallback.toml +++ b/core/chains/evm/config/toml/defaults/fallback.toml @@ -77,7 +77,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' -NewHeadsPollInterval = '0s' +NewHeadsPollInterval = '1s' [OCR] ContractConfirmations = 4 diff --git a/core/web/router.go b/core/web/router.go index 6e96b47981b..82e6c488103 100644 --- a/core/web/router.go +++ b/core/web/router.go @@ -189,9 +189,8 @@ func metricRoutes(r *gin.RouterGroup, includeHeap bool) { pprofGroup.GET("/allocs", ginHandlerFromHTTP(pprof.Handler("allocs").ServeHTTP)) pprofGroup.GET("/block", ginHandlerFromHTTP(pprof.Handler("block").ServeHTTP)) pprofGroup.GET("/goroutine", ginHandlerFromHTTP(pprof.Handler("goroutine").ServeHTTP)) - if includeHeap { - pprofGroup.GET("/heap", ginHandlerFromHTTP(pprof.Handler("heap").ServeHTTP)) - } + pprofGroup.GET("/heap", ginHandlerFromHTTP(pprof.Handler("heap").ServeHTTP)) + pprofGroup.GET("/mutex", ginHandlerFromHTTP(pprof.Handler("mutex").ServeHTTP)) pprofGroup.GET("/threadcreate", ginHandlerFromHTTP(pprof.Handler("threadcreate").ServeHTTP)) } From 612c560252f1cd17684baeca2f87bdf3d9db0fe4 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Wed, 18 Sep 2024 11:54:06 -0500 Subject: [PATCH 21/27] enable for testing --- core/chains/evm/config/toml/defaults/Polygon_Amoy.toml | 1 + core/chains/evm/config/toml/defaults/fallback.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml b/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml index bca42d9b403..a9d148aa52e 100644 --- a/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml +++ b/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml @@ -26,3 +26,4 @@ HistoryDepth = 2000 [NodePool] SyncThreshold = 10 +NewHeadsPollInterval = '1s' diff --git a/core/chains/evm/config/toml/defaults/fallback.toml b/core/chains/evm/config/toml/defaults/fallback.toml index c27d056431e..6f43f956faf 100644 --- a/core/chains/evm/config/toml/defaults/fallback.toml +++ b/core/chains/evm/config/toml/defaults/fallback.toml @@ -77,7 +77,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' -NewHeadsPollInterval = '1s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 From 6a45eee4f7c63a1704c97aafc4f46debc6c838d1 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Wed, 18 Sep 2024 13:09:48 -0500 Subject: [PATCH 22/27] revert --- core/chains/evm/config/toml/defaults/Polygon_Amoy.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml b/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml index a9d148aa52e..bca42d9b403 100644 --- a/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml +++ b/core/chains/evm/config/toml/defaults/Polygon_Amoy.toml @@ -26,4 +26,3 @@ HistoryDepth = 2000 [NodePool] SyncThreshold = 10 -NewHeadsPollInterval = '1s' From d8e59115f6e481c177f9e5c40ed3850599f0a5c0 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Thu, 19 Sep 2024 11:56:53 -0500 Subject: [PATCH 23/27] Dmytro's comments --- .changeset/happy-feet-rhyme.md | 2 +- core/chains/evm/client/rpc_client.go | 23 ++--------------------- core/web/router.go | 5 +++-- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/.changeset/happy-feet-rhyme.md b/.changeset/happy-feet-rhyme.md index be6a32c9ab4..6e1697d96ae 100644 --- a/.changeset/happy-feet-rhyme.md +++ b/.changeset/happy-feet-rhyme.md @@ -3,7 +3,7 @@ --- This PR introduce few changes: -- Add a new flag support called `NewHeadsPollInterval` (0 by default indicate disabled), which is an interval for polling new block periodically using http client rather than subscribe to ws feed. +- Add a new config option `EVM.NodePool.NewHeadsPollInterval` (0 by default indicate disabled), which is an interval for polling new block periodically using http client rather than subscribe to ws feed. - Updated new head handler for polling new head over http, and register the subscription in node lifecycle logic. - If the polling new heads is enabled, WS new heads subscription will be replaced with the new http based polling. diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index cea713e9d99..cd1f01deb48 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -527,7 +527,6 @@ func (r *rpcClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.H lggr := r.newRqLggr().With("args", args) // if new head based on http polling is enabled, we will replace it for WS newHead subscription - lggr.Infof("the newHeadsPollInterval is %v", r.newHeadsPollInterval.String()) if r.newHeadsPollInterval > 0 { interval := r.newHeadsPollInterval timeout := interval @@ -536,7 +535,7 @@ func (r *rpcClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.H return nil, nil, err } - lggr.Infof("Polling new heads over http ") + lggr.Debugf("Polling new heads over http") return channel, &poller, nil } @@ -713,25 +712,7 @@ func (r *rpcClient) LatestFinalizedBlock(ctx context.Context) (head *evmtypes.He } func (r *rpcClient) LatestBlock(ctx context.Context) (head *evmtypes.Head, err error) { - // capture chStopInFlight to ensure we are not updating chainInfo with observations related to previous life cycle - ctx, cancel, chStopInFlight, _, _ := r.acquireQueryCtx(ctx, r.rpcTimeout) - defer cancel() - - // TODO any special treatment for some chains ? - err = r.ethGetBlockByNumber(ctx, rpc.LatestBlockNumber.String(), &head) - if err != nil { - return - } - - if head == nil { - err = r.wrapRPCClientError(ethereum.NotFound) - return - } - - head.EVMChainID = ubig.New(r.chainID) - - r.onNewHead(ctx, chStopInFlight, head) - return + return r.BlockByNumber(ctx, nil) } func (r *rpcClient) astarLatestFinalizedBlock(ctx context.Context, result interface{}) (err error) { diff --git a/core/web/router.go b/core/web/router.go index 82e6c488103..6e96b47981b 100644 --- a/core/web/router.go +++ b/core/web/router.go @@ -189,8 +189,9 @@ func metricRoutes(r *gin.RouterGroup, includeHeap bool) { pprofGroup.GET("/allocs", ginHandlerFromHTTP(pprof.Handler("allocs").ServeHTTP)) pprofGroup.GET("/block", ginHandlerFromHTTP(pprof.Handler("block").ServeHTTP)) pprofGroup.GET("/goroutine", ginHandlerFromHTTP(pprof.Handler("goroutine").ServeHTTP)) - pprofGroup.GET("/heap", ginHandlerFromHTTP(pprof.Handler("heap").ServeHTTP)) - + if includeHeap { + pprofGroup.GET("/heap", ginHandlerFromHTTP(pprof.Handler("heap").ServeHTTP)) + } pprofGroup.GET("/mutex", ginHandlerFromHTTP(pprof.Handler("mutex").ServeHTTP)) pprofGroup.GET("/threadcreate", ginHandlerFromHTTP(pprof.Handler("threadcreate").ServeHTTP)) } From 847613570fcaa3324e080d3195f4914f012dc778 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 20 Sep 2024 14:13:17 -0500 Subject: [PATCH 24/27] make func private --- core/chains/evm/client/rpc_client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index cd1f01deb48..de32ae78605 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -530,7 +530,7 @@ func (r *rpcClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.H if r.newHeadsPollInterval > 0 { interval := r.newHeadsPollInterval timeout := interval - poller, channel := commonclient.NewPoller[*evmtypes.Head](interval, r.LatestBlock, timeout, r.rpcLog) + poller, channel := commonclient.NewPoller[*evmtypes.Head](interval, r.latestBlock, timeout, r.rpcLog) if err = poller.Start(ctx); err != nil { return nil, nil, err } @@ -711,7 +711,7 @@ func (r *rpcClient) LatestFinalizedBlock(ctx context.Context) (head *evmtypes.He return } -func (r *rpcClient) LatestBlock(ctx context.Context) (head *evmtypes.Head, err error) { +func (r *rpcClient) latestBlock(ctx context.Context) (head *evmtypes.Head, err error) { return r.BlockByNumber(ctx, nil) } From 45f18e4e51b81b73addb70fe52f7105a1b0112fe Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 20 Sep 2024 14:41:45 -0500 Subject: [PATCH 25/27] fix test --- docs/CONFIG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CONFIG.md b/docs/CONFIG.md index ae535245301..1512b33cad4 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -4103,6 +4103,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -4205,6 +4206,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 From 2752555e1664361c3fe9a4054c47c538f8027bb7 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 20 Sep 2024 15:50:02 -0500 Subject: [PATCH 26/27] add polling support for SubscribeNewHeaddocke --- core/chains/evm/client/rpc_client.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index de32ae78605..08ff3ff058b 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -493,6 +493,18 @@ func (r *rpcClient) SubscribeNewHead(ctx context.Context, channel chan<- *evmtyp args := []interface{}{"newHeads"} lggr := r.newRqLggr().With("args", args) + if r.newHeadsPollInterval > 0 { + interval := r.newHeadsPollInterval + timeout := interval + poller, _ := commonclient.NewPoller[*evmtypes.Head](interval, r.latestBlock, timeout, r.rpcLog) + if err = poller.Start(ctx); err != nil { + return nil, err + } + + lggr.Infof("Polling new heads over http ") + return &poller, nil + } + lggr.Debug("RPC call: evmclient.Client#EthSubscribe") start := time.Now() defer func() { From 4ccae14459a7e3bca0e41a85a9ae823a9cbfa40e Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 20 Sep 2024 16:10:07 -0500 Subject: [PATCH 27/27] update log level --- core/chains/evm/client/rpc_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 08ff3ff058b..763348173aa 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -501,7 +501,7 @@ func (r *rpcClient) SubscribeNewHead(ctx context.Context, channel chan<- *evmtyp return nil, err } - lggr.Infof("Polling new heads over http ") + lggr.Debugf("Polling new heads over http") return &poller, nil }