Skip to content

Commit 0e76f47

Browse files
authored
Add timeout to orgResolver (#2297)
* Add timeout to orgResolver * update config name * cleanup
1 parent fb115ed commit 0e76f47

2 files changed

Lines changed: 63 additions & 5 deletions

File tree

pkg/services/orgresolver/linking.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"time"
78

89
"go.opentelemetry.io/otel/metric"
910
"google.golang.org/grpc"
@@ -17,6 +18,9 @@ import (
1718
linkingclient "github.com/smartcontractkit/chainlink-protos/linking-service/go/v1"
1819
)
1920

21+
// defaultRequestTimeout bounds Get(); callers sit in capability hot paths, so a hung connection must not propagate.
22+
const defaultRequestTimeout = 2 * time.Second
23+
2024
// JWTGenerator interface for JWT token creation
2125
type JWTGenerator interface {
2226
CreateJWTForRequest(req any) (string, error)
@@ -35,20 +39,23 @@ type Config struct {
3539
WorkflowRegistryChainSelector uint64
3640
JWTGenerator JWTGenerator
3741

42+
RequestTimeout time.Duration // bounds each Get() call; zero means defaultRequestTimeout
43+
3844
Client linkingclient.LinkingServiceClient // optional
3945
Meter metric.Meter // optional
4046
}
4147

4248
// orgResolver makes direct calls to the linking service to resolve organization IDs from workflow owners.
43-
// This simplified implementation makes a network call for each Get() request.
49+
// This simplified implementation makes a network call for each Get() request, bounded by requestTimeout.
4450
type orgResolver struct {
4551
workflowRegistryAddress string
4652
workflowRegistryChainSelector uint64
4753

48-
client linkingclient.LinkingServiceClient
49-
conn *grpc.ClientConn // nil if client was injected
50-
logger log.SugaredLogger
51-
jwtGenerator JWTGenerator
54+
client linkingclient.LinkingServiceClient
55+
conn *grpc.ClientConn // nil if client was injected
56+
logger log.SugaredLogger
57+
jwtGenerator JWTGenerator
58+
requestTimeout time.Duration
5259

5360
passCount metric.Int64Counter
5461
failCount metric.Int64Counter
@@ -72,11 +79,17 @@ func NewOrgResolverWithClient(cfg Config, client linkingclient.LinkingServiceCli
7279
}
7380

7481
func (cfg *Config) New(logger log.Logger) (*orgResolver, error) {
82+
requestTimeout := cfg.RequestTimeout
83+
if requestTimeout <= 0 {
84+
requestTimeout = defaultRequestTimeout
85+
}
86+
7587
resolver := &orgResolver{
7688
workflowRegistryAddress: cfg.WorkflowRegistryAddress,
7789
workflowRegistryChainSelector: cfg.WorkflowRegistryChainSelector,
7890
logger: log.Sugared(logger).Named("OrgResolver"),
7991
jwtGenerator: cfg.JWTGenerator,
92+
requestTimeout: requestTimeout,
8093
}
8194

8295
if cfg.Client != nil {
@@ -135,6 +148,9 @@ func (o *orgResolver) addJWTAuth(ctx context.Context, req any) (context.Context,
135148
}
136149

137150
func (o *orgResolver) Get(ctx context.Context, owner string) (string, error) {
151+
ctx, cancel := context.WithTimeout(ctx, o.requestTimeout)
152+
defer cancel()
153+
138154
req := &linkingclient.GetOrganizationFromWorkflowOwnerRequest{
139155
WorkflowOwner: owner,
140156
WorkflowRegistryAddress: o.workflowRegistryAddress,

pkg/services/orgresolver/linking_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"net"
77
"testing"
8+
"time"
89

910
"github.com/stretchr/testify/require"
1011
"google.golang.org/grpc"
@@ -26,6 +27,14 @@ func (m *mockLinkingClient) GetOrganizationFromWorkflowOwner(ctx context.Context
2627
}, nil
2728
}
2829

30+
// mockHangingLinkingClient blocks until the call context is done, simulating a blackholed connection
31+
type mockHangingLinkingClient struct{}
32+
33+
func (m *mockHangingLinkingClient) GetOrganizationFromWorkflowOwner(ctx context.Context, req *linkingclient.GetOrganizationFromWorkflowOwnerRequest, opts ...grpc.CallOption) (*linkingclient.GetOrganizationFromWorkflowOwnerResponse, error) {
34+
<-ctx.Done()
35+
return nil, ctx.Err()
36+
}
37+
2938
// mockJWTGenerator implements the JWTGenerator interface for testing
3039
type mockJWTGenerator struct {
3140
token string
@@ -89,6 +98,39 @@ func TestOrgResolver_Get(t *testing.T) {
8998
require.Equal(t, "org-"+workflowOwner, orgID)
9099
}
91100

101+
func TestOrgResolver_Get_TimesOutWhenServiceHangs(t *testing.T) {
102+
cfg := Config{
103+
WorkflowRegistryAddress: "0x1234567890abcdef",
104+
WorkflowRegistryChainSelector: 1,
105+
RequestTimeout: 50 * time.Millisecond,
106+
Client: &mockHangingLinkingClient{},
107+
}
108+
109+
resolver, err := cfg.New(logger.Test(t))
110+
require.NoError(t, err)
111+
112+
start := time.Now()
113+
_, err = resolver.Get(t.Context(), "0xabcdef1234567890")
114+
require.ErrorIs(t, err, context.DeadlineExceeded)
115+
require.Less(t, time.Since(start), 5*time.Second)
116+
}
117+
118+
func TestOrgResolver_Get_DefaultTimeoutApplied(t *testing.T) {
119+
cfg := Config{
120+
WorkflowRegistryAddress: "0x1234567890abcdef",
121+
WorkflowRegistryChainSelector: 1,
122+
Client: &mockLinkingClient{},
123+
}
124+
125+
resolver, err := cfg.New(logger.Test(t))
126+
require.NoError(t, err)
127+
require.Equal(t, defaultRequestTimeout, resolver.requestTimeout)
128+
129+
orgID, err := resolver.Get(t.Context(), "0xabcdef1234567890")
130+
require.NoError(t, err)
131+
require.Equal(t, "org-0xabcdef1234567890", orgID)
132+
}
133+
92134
func TestOrgResolver_NewOrgResolver_RequiresClientOrURL(t *testing.T) {
93135
cfg := Config{
94136
URL: "", // Empty URL should cause error

0 commit comments

Comments
 (0)