Skip to content

Commit

Permalink
BCF-2684: copy types from core to support relayer implementations (#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmank88 authored Oct 4, 2023
1 parent 5e58116 commit 02b76df
Show file tree
Hide file tree
Showing 26 changed files with 920 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/golangci_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.21'

- name: Install golangci-lint
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.3
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
golang 1.20.4
golang 1.21.1
golangci-lint 1.51.1
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/smartcontractkit/chainlink-relay

go 1.20
go 1.21

require (
github.com/confluentinc/confluent-kafka-go v1.9.2
Expand Down
2 changes: 1 addition & 1 deletion ops/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/smartcontractkit/chainlink-relay/ops

go 1.20
go 1.21

require (
github.com/lib/pq v1.10.4
Expand Down
97 changes: 97 additions & 0 deletions pkg/chains/nodes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package chains

import (
"encoding/base64"
"errors"
"fmt"
"net/url"
"strconv"

"github.com/smartcontractkit/chainlink-relay/pkg/types"
)

// pageToken is simple internal representation for coordination requests and responses in a paginated API
// It is inspired by the Google API Design patterns
// https://cloud.google.com/apis/design/design_patterns#list_pagination
// https://google.aip.dev/158
type pageToken struct {
Page int
Size int
}

var (
ErrInvalidToken = errors.New("invalid page token")
ErrOutOfRange = errors.New("out of range")
defaultSize = 100
)

// Encode the token in base64 for transmission for the wire
func (pr *pageToken) Encode() string {
if pr.Size == 0 {
pr.Size = defaultSize
}
// this is a simple minded implementation and may benefit from something fancier
// note that this is a valid url.Query string, which we leverage in decoding
s := fmt.Sprintf("page=%d&size=%d", pr.Page, pr.Size)
return base64.RawStdEncoding.EncodeToString([]byte(s))
}

// b64enc must be the base64 encoded token string, corresponding to [pageToken.Encode()]
func NewPageToken(b64enc string) (*pageToken, error) {
// empty is valid
if b64enc == "" {
return &pageToken{Page: 0, Size: defaultSize}, nil
}

b, err := base64.RawStdEncoding.DecodeString(b64enc)
if err != nil {
return nil, err
}
// here too, this is simple minded and could be fancier

vals, err := url.ParseQuery(string(b))
if err != nil {
return nil, err
}
if !(vals.Has("page") && vals.Has("size")) {
return nil, ErrInvalidToken
}
page, err := strconv.Atoi(vals.Get("page"))
if err != nil {
return nil, fmt.Errorf("%w: bad page", ErrInvalidToken)
}
size, err := strconv.Atoi(vals.Get("size"))
if err != nil {
return nil, fmt.Errorf("%w: bad size", ErrInvalidToken)
}
return &pageToken{
Page: page,
Size: size,
}, err
}

// if start is out of range, must return ErrOutOfRange
type ListNodeStatusFn = func(start, end int) (stats []types.NodeStatus, total int, err error)

func ListNodeStatuses(pageSize int, pageTokenStr string, listFn ListNodeStatusFn) (stats []types.NodeStatus, nextPageToken string, total int, err error) {
if pageSize == 0 {
pageSize = defaultSize
}
t := &pageToken{Page: 0, Size: pageSize}
if pageTokenStr != "" {
t, err = NewPageToken(pageTokenStr)
if err != nil {
return nil, "", -1, err
}
}
start, end := t.Page*t.Size, (t.Page+1)*t.Size
stats, total, err = listFn(start, end)
if err != nil {
return stats, "", -1, err
}
if total > end {
next_token := &pageToken{Page: t.Page + 1, Size: t.Size}
nextPageToken = next_token.Encode()
}
return stats, nextPageToken, total, nil
}
166 changes: 166 additions & 0 deletions pkg/chains/nodes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package chains

import (
"encoding/base64"
"fmt"
"reflect"
"testing"

"github.com/stretchr/testify/assert"

"github.com/smartcontractkit/chainlink-relay/pkg/types"
)

func TestNewPageToken(t *testing.T) {
type args struct {
t *pageToken
}
tests := []struct {
name string
args args
want *pageToken
wantErr bool
}{
{
name: "empty",
args: args{t: &pageToken{}},
want: &pageToken{Page: 0, Size: defaultSize},
},
{
name: "page set, size unset",
args: args{t: &pageToken{Page: 1}},
want: &pageToken{Page: 1, Size: defaultSize},
},
{
name: "page set, size set",
args: args{t: &pageToken{Page: 3, Size: 10}},
want: &pageToken{Page: 3, Size: 10},
},
{
name: "page unset, size set",
args: args{t: &pageToken{Size: 17}},
want: &pageToken{Page: 0, Size: 17},
},
}
for _, tt := range tests {
enc := tt.args.t.Encode()
t.Run(tt.name, func(t *testing.T) {
got, err := NewPageToken(enc)
if (err != nil) != tt.wantErr {
t.Errorf("NewPageToken() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewPageToken() = %v, want %v", got, tt.want)
}
})
}
}

func TestListNodeStatuses(t *testing.T) {
testStats := []types.NodeStatus{
{
ChainID: "chain-1",
Name: "name-1",
},
{
ChainID: "chain-2",
Name: "name-2",
},
{
ChainID: "chain-3",
Name: "name-3",
},
}

type args struct {
pageSize int
pageToken string
listFn ListNodeStatusFn
}
tests := []struct {
name string
args args
wantStats []types.NodeStatus
wantNextPageToken string
wantTotal int
wantErr bool
}{
{
name: "all on first page",
args: args{
pageSize: 10, // > length of test stats
pageToken: "",
listFn: func(start, end int) ([]types.NodeStatus, int, error) {
return testStats, len(testStats), nil
},
},
wantNextPageToken: "",
wantTotal: len(testStats),
wantStats: testStats,
},
{
name: "small first page",
args: args{
pageSize: len(testStats) - 1,
pageToken: "",
listFn: func(start, end int) ([]types.NodeStatus, int, error) {
return testStats[start:end], len(testStats), nil
},
},
wantNextPageToken: base64.RawStdEncoding.EncodeToString([]byte("page=1&size=2")), // hard coded 2 is len(testStats)-1
wantTotal: len(testStats),
wantStats: testStats[0 : len(testStats)-1],
},
{
name: "second page",
args: args{
pageSize: len(testStats) - 1,
pageToken: base64.RawStdEncoding.EncodeToString([]byte("page=1&size=2")), // hard coded 2 is len(testStats)-1
listFn: func(start, end int) ([]types.NodeStatus, int, error) {
// note list function must do the start, end bound checking. here we are making it simple
if end > len(testStats) {
end = len(testStats)
}
return testStats[start:end], len(testStats), nil
},
},
wantNextPageToken: "",
wantTotal: len(testStats),
wantStats: testStats[len(testStats)-1:],
},
{
name: "bad list fn",
args: args{
listFn: func(start, end int) ([]types.NodeStatus, int, error) {
return nil, 0, fmt.Errorf("i'm a bad list fn")
},
},
wantTotal: -1,
wantErr: true,
},
{
name: "invalid token",
args: args{
pageToken: "invalid token",
listFn: func(start, end int) ([]types.NodeStatus, int, error) {
return testStats[start:end], len(testStats), nil
},
},
wantTotal: -1,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotStats, gotNext_pageToken, gotTotal, err := ListNodeStatuses(tt.args.pageSize, tt.args.pageToken, tt.args.listFn)
if (err != nil) != tt.wantErr {
t.Errorf("ListNodeStatuses() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.wantStats, gotStats)
assert.Equal(t, tt.wantNextPageToken, gotNext_pageToken)
assert.Equal(t, tt.wantTotal, gotTotal)
})
}
}
47 changes: 45 additions & 2 deletions pkg/config/error.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package config

import "fmt"
import (
"fmt"
"reflect"
)

// lightweight error types copied from core

Expand All @@ -11,7 +14,12 @@ type ErrInvalid struct {
}

func (e ErrInvalid) Error() string {
return fmt.Sprintf("%s: invalid value %v: %s", e.Name, e.Value, e.Msg)
return fmt.Sprintf("%s: invalid value (%v): %s", e.Name, e.Value, e.Msg)
}

// NewErrDuplicate returns an ErrInvalid with a standard duplicate message.
func NewErrDuplicate(name string, value any) ErrInvalid {
return ErrInvalid{Name: name, Value: value, Msg: "duplicate - must be unique"}
}

type ErrMissing struct {
Expand Down Expand Up @@ -40,3 +48,38 @@ type KeyNotFoundError struct {
func (e KeyNotFoundError) Error() string {
return fmt.Sprintf("unable to find %s key with id %s", e.KeyType, e.ID)
}

// UniqueStrings is a helper for tracking unique values in string form.
type UniqueStrings map[string]struct{}

// IsDupeFmt is like IsDupe, but calls String().
func (u UniqueStrings) IsDupeFmt(t fmt.Stringer) bool {
if t == nil {
return false
}
if reflect.ValueOf(t).IsNil() {
// interface holds a typed-nil value
return false
}
return u.isDupe(t.String())
}

// IsDupe returns true if the set already contains the string, otherwise false.
// Non-nil/empty strings are added to the set.
func (u UniqueStrings) IsDupe(s *string) bool {
if s == nil {
return false
}
return u.isDupe(*s)
}

func (u UniqueStrings) isDupe(s string) bool {
if s == "" {
return false
}
_, ok := u[s]
if !ok {
u[s] = struct{}{}
}
return ok
}
2 changes: 1 addition & 1 deletion pkg/loop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ sequenceDiagram

The `pluginService` type contains reusable automatic recovery code.

`type pluginService[P grpcPlugin, S types.Service] struct`
`type pluginService[P grpcPlugin, S services.Service] struct`

Each plugin implements their own interface (Relayer, Median, etc.) with a new type that also embeds a `pluginService`.
This new **service** type implements the original interface, but internally manages re-starting and re-connecting to the plugin
Expand Down
6 changes: 3 additions & 3 deletions pkg/loop/internal/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import (
"google.golang.org/protobuf/types/known/emptypb"

"github.com/smartcontractkit/chainlink-relay/pkg/loop/internal/pb"
"github.com/smartcontractkit/chainlink-relay/pkg/types"
"github.com/smartcontractkit/chainlink-relay/pkg/services"
)

var _ types.Service = (*serviceClient)(nil)
var _ services.Service = (*serviceClient)(nil)

type serviceClient struct {
b *brokerExt
Expand Down Expand Up @@ -66,7 +66,7 @@ var _ pb.ServiceServer = (*serviceServer)(nil)

type serviceServer struct {
pb.UnimplementedServiceServer
srv types.Service
srv services.Service
}

func (s *serviceServer) Close(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
Expand Down
Loading

0 comments on commit 02b76df

Please sign in to comment.