From 7c6d505d1da31209aa8c3831dfdfe071fde6ebbe Mon Sep 17 00:00:00 2001 From: Michael Pratt Date: Mon, 28 Sep 2026 19:18:37 +0000 Subject: [PATCH] internal: migrate CancelRequest to context deadline In upcoming Go 1.28, the deprecated Transport.CancelRequest has become no-op, causing TestAPICallRPCFailure to timeout because the in-flight request is never canceled. Set the deadline using context.WithTimeout instead, matching the current recommended way of canceling request. Note that this also means that cancelling the context passed to post will cancel the HTTP request, which it previously would not. Creating a context with timeout has a few more allocations than creating a timer + closure, so bump the limits on TestAPICallAllocations. Part of this is due to additional allocations from extending the parent context with an additional timeout. For what it's worth, I'm not convinced that absolute allocation count is the best metric here, given how many dependencies Call has (net/http, protobuf, etc). The v2 version of this test is not failing, but it's at exactly 86, so bump it so the next tiny change doesn't break it. --- internal/api.go | 25 +++++++++---------------- internal/api_test.go | 4 ++-- v2/internal/api.go | 25 +++++++++---------------- v2/internal/api_test.go | 2 +- 4 files changed, 21 insertions(+), 35 deletions(-) diff --git a/internal/api.go b/internal/api.go index 0569f5dd..0f0009db 100644 --- a/internal/api.go +++ b/internal/api.go @@ -22,7 +22,6 @@ import ( "strconv" "strings" "sync" - "sync/atomic" "time" "github.com/golang/protobuf/proto" @@ -359,6 +358,14 @@ func (c *aeContext) WriteHeader(code int) { } func post(ctx context.Context, body []byte, timeout time.Duration) (b []byte, err error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + defer func() { + if ctx.Err() == context.DeadlineExceeded { + err = errTimeout + } + }() + apiURL := apiURL(ctx) hreq := &http.Request{ Method: "POST", @@ -373,6 +380,7 @@ func post(ctx context.Context, body []byte, timeout time.Duration) (b []byte, er ContentLength: int64(len(body)), Host: apiURL.Host, } + hreq = hreq.WithContext(ctx) c := fromContext(ctx) if c != nil { if info := c.req.Header.Get(dapperHeader); info != "" { @@ -383,21 +391,6 @@ func post(ctx context.Context, body []byte, timeout time.Duration) (b []byte, er } } - tr := apiHTTPClient.Transport.(*http.Transport) - - var timedOut int32 // atomic; set to 1 if timed out - t := time.AfterFunc(timeout, func() { - atomic.StoreInt32(&timedOut, 1) - tr.CancelRequest(hreq) - }) - defer t.Stop() - defer func() { - // Check if timeout was exceeded. - if atomic.LoadInt32(&timedOut) != 0 { - err = errTimeout - } - }() - hresp, err := apiHTTPClient.Do(hreq) if err != nil { return nil, &CallError{ diff --git a/internal/api_test.go b/internal/api_test.go index 9692b29e..f95cde4f 100644 --- a/internal/api_test.go +++ b/internal/api_test.go @@ -468,11 +468,11 @@ func TestAPICallAllocations(t *testing.T) { } // Lots of room for improvement... - var min, max float64 = 60, 86 + var min, max float64 = 60, 100 if strings.HasPrefix(runtime.Version(), "go1.11.") || strings.HasPrefix(runtime.Version(), "go1.12.") { // add a bit more overhead for versions before go1.13 // see https://go.dev/doc/go1.13#compilers - max = 90 + max += 10 } if avg < min || max < avg { t.Errorf("Allocations per API call = %g, want in [%g,%g]", avg, min, max) diff --git a/v2/internal/api.go b/v2/internal/api.go index 44314a2b..9ccdd261 100644 --- a/v2/internal/api.go +++ b/v2/internal/api.go @@ -17,7 +17,6 @@ import ( "os" "runtime" "strconv" - "sync/atomic" "time" "github.com/golang/protobuf/proto" @@ -315,6 +314,14 @@ func (c *aeContext) WriteHeader(code int) { } func post(ctx context.Context, body []byte, timeout time.Duration) (b []byte, err error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + defer func() { + if ctx.Err() == context.DeadlineExceeded { + err = errTimeout + } + }() + apiURL := apiURL(ctx) hreq := &http.Request{ Method: "POST", @@ -329,6 +336,7 @@ func post(ctx context.Context, body []byte, timeout time.Duration) (b []byte, er ContentLength: int64(len(body)), Host: apiURL.Host, } + hreq = hreq.WithContext(ctx) c := fromContext(ctx) if c != nil { if info := c.req.Header.Get(dapperHeader); info != "" { @@ -339,21 +347,6 @@ func post(ctx context.Context, body []byte, timeout time.Duration) (b []byte, er } } - tr := apiHTTPClient.Transport.(*http.Transport) - - var timedOut int32 // atomic; set to 1 if timed out - t := time.AfterFunc(timeout, func() { - atomic.StoreInt32(&timedOut, 1) - tr.CancelRequest(hreq) - }) - defer t.Stop() - defer func() { - // Check if timeout was exceeded. - if atomic.LoadInt32(&timedOut) != 0 { - err = errTimeout - } - }() - hresp, err := apiHTTPClient.Do(hreq) if err != nil { return nil, &CallError{ diff --git a/v2/internal/api_test.go b/v2/internal/api_test.go index 073e9bfd..0979dea4 100644 --- a/v2/internal/api_test.go +++ b/v2/internal/api_test.go @@ -324,7 +324,7 @@ func TestAPICallAllocations(t *testing.T) { } // Lots of room for improvement... - const min, max float64 = 60, 86 + const min, max float64 = 60, 100 if avg < min || max < avg { t.Errorf("Allocations per API call = %g, want in [%g,%g]", avg, min, max) }