Skip to content

Commit 6cff0a9

Browse files
committed
feat(multiclient): make sure parent context deadlines are used
1 parent 3566c19 commit 6cff0a9

File tree

2 files changed

+57
-1
lines changed

2 files changed

+57
-1
lines changed

deployment/multiclient.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,9 @@ func (mc *MultiClient) SubscribeFilterLogs(ctx context.Context, q ethereum.Filte
286286
return sub, err
287287
}
288288

289+
// WaitMined waits for a transaction to be mined and returns the receipt.
290+
// A timeout for this operation must be set in the context.
291+
// Note: retryConfig timeout settings are not used for this operation.
289292
func (mc *MultiClient) WaitMined(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) {
290293
mc.lggr.Debugf("Waiting for tx %s to be mined for chain %s", tx.Hash().Hex(), mc.chainName)
291294
// no retries here because we want to wait for the tx to be mined
@@ -330,7 +333,7 @@ func (mc *MultiClient) retryWithBackups(ctx context.Context, opName string, op f
330333
for i, client := range append([]*ethclient.Client{mc.Client}, mc.Backups...) {
331334
retryCount := 0
332335
err2 := retry.Do(func() error {
333-
timeoutCtx, cancel := context.WithTimeout(ctx, mc.RetryConfig.Timeout)
336+
timeoutCtx, cancel := ensureTimeout(ctx, mc.RetryConfig.Timeout)
334337
defer cancel()
335338

336339
err = op(timeoutCtx, client)
@@ -389,3 +392,17 @@ func (mc *MultiClient) dialWithRetry(rpc RPC, lggr logger.Logger) (*ethclient.Cl
389392

390393
return client, nil
391394
}
395+
396+
// ensureTimeout checks if the parent context has a deadline.
397+
// If it does, it returns a new cancelable context using the parent's deadline.
398+
// If it doesn't, it creates a new context with the specified timeout.
399+
func ensureTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
400+
// check if the parent context already has a deadline
401+
if _, hasDeadline := parent.Deadline(); hasDeadline {
402+
// derive a new cancelable context from the parent context with the same deadline
403+
return context.WithCancel(parent)
404+
}
405+
406+
// create a new context with the specified timeout
407+
return context.WithTimeout(parent, timeout)
408+
}

deployment/multiclient_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,42 @@ func TestMultiClient_retryWithBackups(t *testing.T) {
180180
})
181181
}
182182
}
183+
184+
func TestEnsureTimeout(t *testing.T) {
185+
t.Parallel()
186+
187+
tests := []struct {
188+
name string
189+
parentContext context.Context
190+
timeout time.Duration
191+
}{
192+
{
193+
name: "Parent context with deadline",
194+
parentContext: func() context.Context { ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute); return ctx }(),
195+
timeout: 1 * time.Minute,
196+
},
197+
{
198+
name: "Parent context without deadline",
199+
parentContext: context.Background(),
200+
timeout: 1 * time.Minute,
201+
},
202+
}
203+
204+
for _, tt := range tests {
205+
t.Run(tt.name, func(t *testing.T) {
206+
t.Parallel()
207+
208+
ctx, cancelFunc := ensureTimeout(tt.parentContext, tt.timeout)
209+
defer cancelFunc()
210+
211+
deadline, hasDeadline := ctx.Deadline()
212+
require.True(t, hasDeadline, "Expected context to have a deadline")
213+
214+
if parentDeadline, hasParentDeadline := tt.parentContext.Deadline(); hasParentDeadline {
215+
require.WithinDuration(t, parentDeadline, deadline, 0, "Deadline should match parent's deadline")
216+
} else {
217+
require.WithinDuration(t, time.Now().Add(tt.timeout), deadline, 50*time.Millisecond, "Deadline should be approximately the specified timeout")
218+
}
219+
})
220+
}
221+
}

0 commit comments

Comments
 (0)