Commit 5960102c authored by Andreas Bigger's avatar Andreas Bigger

Backoff client refactored as a service.

parent 5d9a38dc
package client
import (
"context"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-service/backoff"
)
var (
// ExponentialBackoff is the default backoff strategy.
ExponentialBackoff = backoff.Exponential()
)
// InnerRPC is a minimal [client.RPC] interface that is used by the backoff client.
//
//go:generate mockery --name InnerRPC --output ./mocks/
type InnerRPC interface {
Close()
CallContext(ctx context.Context, result any, method string, args ...any) error
BatchCallContext(ctx context.Context, b []rpc.BatchElem) error
EthSubscribe(ctx context.Context, channel any, args ...any) (ethereum.Subscription, error)
}
// backoffEthClient wraps a [InnerRPC] with a backoff strategy.
type backoffEthClient struct {
c InnerRPC
retryAttempts int
strategy backoff.Strategy
}
// NewBackoffClient creates a new backoff client.
// The backoff strategy is optional, if not provided, the default exponential backoff strategy is used.
func NewBackoffClient(c InnerRPC, retries int, strategy ...backoff.Strategy) *backoffEthClient {
if len(strategy) == 0 {
strategy = []backoff.Strategy{ExponentialBackoff}
}
return &backoffEthClient{
c: c,
retryAttempts: retries,
strategy: strategy[0],
}
}
// BackoffStrategy returns the [backoff.Strategy] used by the client.
func (b *backoffEthClient) BackoffStrategy() backoff.Strategy {
return b.strategy
}
func (b *backoffEthClient) Close() {
b.c.Close()
}
func (b *backoffEthClient) CallContext(ctx context.Context, result any, method string, args ...any) error {
return backoff.DoCtx(ctx, b.retryAttempts, b.strategy, func() error {
cCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
return b.c.CallContext(cCtx, result, method, args...)
})
}
func (b *backoffEthClient) BatchCallContext(ctx context.Context, batch []rpc.BatchElem) error {
return backoff.DoCtx(ctx, b.retryAttempts, b.strategy, func() error {
cCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
err := b.c.BatchCallContext(cCtx, batch)
return err
})
}
func (b *backoffEthClient) EthSubscribe(ctx context.Context, channel any, args ...any) (ethereum.Subscription, error) {
var sub ethereum.Subscription
err := backoff.DoCtx(ctx, b.retryAttempts, b.strategy, func() error {
var err error
sub, err = b.c.EthSubscribe(ctx, channel, args...)
return err
})
return sub, err
}
package client_test
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-service/backoff"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/client/mocks"
)
func TestClient_BackoffClient_Strategy(t *testing.T) {
mockRpc := mocks.NewInnerRPC(t)
backoffClient := client.NewBackoffClient(mockRpc, 0)
require.Equal(t, backoffClient.BackoffStrategy(), client.ExponentialBackoff)
fixedStrategy := &backoff.FixedStrategy{}
backoffClient = client.NewBackoffClient(mockRpc, 0, fixedStrategy)
require.Equal(t, backoffClient.BackoffStrategy(), fixedStrategy)
}
func TestClient_BackoffClient_Close(t *testing.T) {
mockRpc := mocks.NewInnerRPC(t)
mockRpc.On("Close").Return()
backoffClient := client.NewBackoffClient(mockRpc, 0)
backoffClient.Close()
require.True(t, mockRpc.AssertCalled(t, "Close"))
}
func TestClient_BackoffClient_CallContext(t *testing.T) {
mockRpc := mocks.NewInnerRPC(t)
mockRpc.On("CallContext", mock.Anything, nil, "foo", "bar").Return(nil)
backoffClient := client.NewBackoffClient(mockRpc, 1)
err := backoffClient.CallContext(context.Background(), nil, "foo", "bar")
require.NoError(t, err)
require.True(t, mockRpc.AssertCalled(t, "CallContext", mock.Anything, nil, "foo", "bar"))
}
func TestClient_BackoffClient_CallContext_WithRetries(t *testing.T) {
mockRpc := mocks.NewInnerRPC(t)
mockRpc.On("CallContext", mock.Anything, nil, "foo", "bar").Return(errors.New("foo"))
backoffClient := client.NewBackoffClient(mockRpc, 2)
err := backoffClient.CallContext(context.Background(), nil, "foo", "bar")
require.Error(t, err)
require.True(t, mockRpc.AssertNumberOfCalls(t, "CallContext", 2))
}
func TestClient_BackoffClient_BatchCallContext(t *testing.T) {
mockRpc := mocks.NewInnerRPC(t)
mockRpc.On("BatchCallContext", mock.Anything, []rpc.BatchElem(nil)).Return(nil)
backoffClient := client.NewBackoffClient(mockRpc, 1)
err := backoffClient.BatchCallContext(context.Background(), nil)
require.NoError(t, err)
require.True(t, mockRpc.AssertCalled(t, "BatchCallContext", mock.Anything, []rpc.BatchElem(nil)))
}
func TestClient_BackoffClient_BatchCallContext_WithRetries(t *testing.T) {
mockRpc := mocks.NewInnerRPC(t)
mockRpc.On("BatchCallContext", mock.Anything, []rpc.BatchElem(nil)).Return(errors.New("foo"))
backoffClient := client.NewBackoffClient(mockRpc, 2)
err := backoffClient.BatchCallContext(context.Background(), nil)
require.Error(t, err)
require.True(t, mockRpc.AssertNumberOfCalls(t, "BatchCallContext", 2))
}
func TestClient_BackoffClient_EthSubscribe(t *testing.T) {
mockRpc := mocks.NewInnerRPC(t)
mockRpc.On("EthSubscribe", mock.Anything, nil, "foo", "bar").Return(nil, nil)
backoffClient := client.NewBackoffClient(mockRpc, 1)
_, err := backoffClient.EthSubscribe(context.Background(), nil, "foo", "bar")
require.NoError(t, err)
require.True(t, mockRpc.AssertCalled(t, "EthSubscribe", mock.Anything, nil, "foo", "bar"))
}
func TestClient_BackoffClient_EthSubscribe_WithRetries(t *testing.T) {
mockRpc := mocks.NewInnerRPC(t)
mockRpc.On("EthSubscribe", mock.Anything, nil, "foo", "bar").Return(nil, errors.New("foo"))
backoffClient := client.NewBackoffClient(mockRpc, 2)
_, err := backoffClient.EthSubscribe(context.Background(), nil, "foo", "bar")
require.Error(t, err)
require.True(t, mockRpc.AssertNumberOfCalls(t, "EthSubscribe", 2))
}
// Code generated by mockery v2.28.1. DO NOT EDIT.
package mocks
import (
context "context"
ethereum "github.com/ethereum/go-ethereum"
mock "github.com/stretchr/testify/mock"
rpc "github.com/ethereum/go-ethereum/rpc"
)
// InnerRPC is an autogenerated mock type for the InnerRPC type
type InnerRPC struct {
mock.Mock
}
// BatchCallContext provides a mock function with given fields: ctx, b
func (_m *InnerRPC) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
ret := _m.Called(ctx, b)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, []rpc.BatchElem) error); ok {
r0 = rf(ctx, b)
} else {
r0 = ret.Error(0)
}
return r0
}
// CallContext provides a mock function with given fields: ctx, result, method, args
func (_m *InnerRPC) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
var _ca []interface{}
_ca = append(_ca, ctx, result, method)
_ca = append(_ca, args...)
ret := _m.Called(_ca...)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, interface{}, string, ...interface{}) error); ok {
r0 = rf(ctx, result, method, args...)
} else {
r0 = ret.Error(0)
}
return r0
}
// Close provides a mock function with given fields:
func (_m *InnerRPC) Close() {
_m.Called()
}
// EthSubscribe provides a mock function with given fields: ctx, channel, args
func (_m *InnerRPC) EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (ethereum.Subscription, error) {
var _ca []interface{}
_ca = append(_ca, ctx, channel)
_ca = append(_ca, args...)
ret := _m.Called(_ca...)
var r0 ethereum.Subscription
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, interface{}, ...interface{}) (ethereum.Subscription, error)); ok {
return rf(ctx, channel, args...)
}
if rf, ok := ret.Get(0).(func(context.Context, interface{}, ...interface{}) ethereum.Subscription); ok {
r0 = rf(ctx, channel, args...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(ethereum.Subscription)
}
}
if rf, ok := ret.Get(1).(func(context.Context, interface{}, ...interface{}) error); ok {
r1 = rf(ctx, channel, args...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewInnerRPC interface {
mock.TestingT
Cleanup(func())
}
// NewInnerRPC creates a new instance of InnerRPC. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewInnerRPC(t mockConstructorTestingTNewInnerRPC) *InnerRPC {
mock := &InnerRPC{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment