Skip to content

Commit dcdbe38

Browse files
committed
feat: add retry attempt headers to every request
Lets a server tell a retry attempt apart from a request's initial attempt. - Request now carries an InvocationID generated once per logical call and kept across all of its retry attempts - AddRetryInfoHeaderHandler sets X-Sdk-Invocation-Id (stable across retries) and X-Sdk-Request: attempt=N; max=M (updated on every attempt) on Handlers.Sign, which re-runs on every retry - Registered by default for all services via defaults.Handlers() - Both headers are set before signing runs, so they end up covered by the request's signature (verified against vefaas.CreateSandboxRequest)
1 parent 9db25a2 commit dcdbe38

5 files changed

Lines changed: 186 additions & 25 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package vefaas_test
2+
3+
// Confirms the retry attempt headers (X-Sdk-Invocation-Id, X-Sdk-Request) are
4+
// present before signing runs, so they end up covered by the request's
5+
// signature (Authorization: ...SignedHeaders=...) instead of being appended
6+
// after signing.
7+
8+
import (
9+
"strings"
10+
"testing"
11+
12+
"github.com/volcengine/volcengine-go-sdk/service/vefaas"
13+
"github.com/volcengine/volcengine-go-sdk/volcengine"
14+
"github.com/volcengine/volcengine-go-sdk/volcengine/credentials"
15+
"github.com/volcengine/volcengine-go-sdk/volcengine/session"
16+
)
17+
18+
func TestCreateSandboxRequestSignsRetryHeaders(t *testing.T) {
19+
sess, err := session.NewSession(volcengine.NewConfig().
20+
WithRegion("cn-beijing").
21+
WithEndpoint("https://example.com").
22+
WithCredentials(credentials.NewStaticCredentials("test-ak", "test-sk", "")))
23+
if err != nil {
24+
t.Fatalf("failed to create session: %v", err)
25+
}
26+
27+
client := vefaas.New(sess)
28+
req, _ := client.CreateSandboxRequest(&vefaas.CreateSandboxInput{
29+
FunctionId: volcengine.String("fn-test"),
30+
})
31+
32+
if err := req.Sign(); err != nil {
33+
t.Fatalf("failed to sign request: %v", err)
34+
}
35+
36+
if req.HTTPRequest.Header.Get("X-Sdk-Invocation-Id") == "" {
37+
t.Fatal("X-Sdk-Invocation-Id header missing before signing")
38+
}
39+
if req.HTTPRequest.Header.Get("X-Sdk-Request") == "" {
40+
t.Fatal("X-Sdk-Request header missing before signing")
41+
}
42+
43+
auth := req.HTTPRequest.Header.Get("Authorization")
44+
if auth == "" {
45+
t.Fatal("Authorization header missing after Sign()")
46+
}
47+
48+
idx := strings.Index(auth, "SignedHeaders=")
49+
if idx == -1 {
50+
t.Fatalf("Authorization header has no SignedHeaders: %s", auth)
51+
}
52+
signedHeaders := auth[idx:]
53+
if end := strings.Index(signedHeaders, ","); end != -1 {
54+
signedHeaders = signedHeaders[:end]
55+
}
56+
57+
for _, want := range []string{"x-sdk-invocation-id", "x-sdk-request"} {
58+
if !strings.Contains(signedHeaders, want) {
59+
t.Errorf("SignedHeaders = %q, want it to contain %q", signedHeaders, want)
60+
}
61+
}
62+
}

volcengine/corehandlers/handlers.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ type lener interface {
2424
Len() int
2525
}
2626

27+
const (
28+
// retryInvocationIDHeader carries an id that is generated once per
29+
// logical request and stays the same across all of its retry attempts,
30+
// so a server can group attempts that belong to the same call.
31+
retryInvocationIDHeader = "X-Sdk-Invocation-Id"
32+
33+
// retryAttemptHeader carries the current attempt count (1 for the
34+
// initial try) and the maximum number of attempts the SDK will make,
35+
// so a server can tell a retry attempt apart from the initial request.
36+
retryAttemptHeader = "X-Sdk-Request"
37+
)
38+
39+
// AddRetryInfoHeaderHandler annotates every HTTP attempt with the request's
40+
// invocation id and its attempt/max-attempt count. It runs on every retry
41+
// (it is registered on Handlers.Sign, which is re-run for each attempt),
42+
// so the attempt count in the header always reflects the current try.
43+
var AddRetryInfoHeaderHandler = request.NamedHandler{
44+
Name: "core.AddRetryInfoHeaderHandler",
45+
Fn: func(r *request.Request) {
46+
r.HTTPRequest.Header.Set(retryInvocationIDHeader, r.InvocationID)
47+
r.HTTPRequest.Header.Set(retryAttemptHeader,
48+
fmt.Sprintf("attempt=%d; max=%d", r.RetryCount+1, r.MaxRetries()+1))
49+
},
50+
}
51+
2752
// BuildContentLengthHandler builds the content length of a request based on the volcenginebody,
2853
// or will use the HTTPRequest.Header's "Content-Length" if defined. If unable
2954
// to determine request volcenginebody length and no "Content-Length" was specified it will panic.

volcengine/defaults/defaults.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func Handlers() request.Handlers {
7676
handlers.Build.PushBackNamed(corehandlers.CustomerRequestHandler)
7777
handlers.Build.AfterEachFn = request.HandlerListStopOnError
7878
handlers.Sign.PushBackNamed(corehandlers.BuildContentLengthHandler)
79+
handlers.Sign.PushBackNamed(corehandlers.AddRetryInfoHeaderHandler)
7980
handlers.Send.PushBackNamed(corehandlers.ValidateReqSigHandler)
8081
handlers.Send.PushBackNamed(corehandlers.SendHandler)
8182
handlers.AfterRetry.PushBackNamed(corehandlers.AfterRetryHandler)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package defaults_test
2+
3+
// Verifies the retry attempt headers added to volcengine/corehandlers:
4+
// X-Sdk-Invocation-Id must stay the same across every retry attempt of a single
5+
// logical request, and X-Sdk-Request must report the current/max attempt count.
6+
7+
import (
8+
"net/http"
9+
"net/http/httptest"
10+
"testing"
11+
12+
"github.com/volcengine/volcengine-go-sdk/volcengine/client"
13+
"github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata"
14+
"github.com/volcengine/volcengine-go-sdk/volcengine/defaults"
15+
"github.com/volcengine/volcengine-go-sdk/volcengine/request"
16+
)
17+
18+
func TestAddRetryInfoHeaderAcrossRetries(t *testing.T) {
19+
var invocationIDs []string
20+
var attemptHeaders []string
21+
callCount := 0
22+
23+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24+
callCount++
25+
invocationIDs = append(invocationIDs, r.Header.Get("X-Sdk-Invocation-Id"))
26+
attemptHeaders = append(attemptHeaders, r.Header.Get("X-Sdk-Request"))
27+
if callCount < 3 {
28+
w.WriteHeader(http.StatusInternalServerError)
29+
return
30+
}
31+
w.WriteHeader(http.StatusOK)
32+
}))
33+
defer server.Close()
34+
35+
cfg := defaults.Config().WithHTTPClient(server.Client()).WithRegion("test-region")
36+
handlers := defaults.Handlers()
37+
38+
c := client.New(*cfg, metadata.ClientInfo{Endpoint: server.URL, SigningRegion: "test-region"}, handlers)
39+
c.Retryer = client.DefaultRetryer{NumMaxRetries: 2}
40+
41+
req := c.NewRequest(&request.Operation{Name: "Test", HTTPMethod: "POST", HTTPPath: "/"}, &struct{}{}, &struct{}{})
42+
if err := req.Send(); err != nil {
43+
t.Fatalf("expected success after retries, got error: %v", err)
44+
}
45+
46+
if callCount != 3 {
47+
t.Fatalf("expected 3 attempts (1 initial + 2 retries), got %d", callCount)
48+
}
49+
50+
for i, id := range invocationIDs {
51+
if id == "" {
52+
t.Fatalf("attempt %d: X-Sdk-Invocation-Id header missing", i+1)
53+
}
54+
if id != invocationIDs[0] {
55+
t.Fatalf("attempt %d: X-Sdk-Invocation-Id changed across retries: %q vs %q", i+1, id, invocationIDs[0])
56+
}
57+
}
58+
59+
wantAttempts := []string{"attempt=1; max=3", "attempt=2; max=3", "attempt=3; max=3"}
60+
for i, want := range wantAttempts {
61+
if attemptHeaders[i] != want {
62+
t.Errorf("attempt %d: X-Sdk-Request = %q, want %q", i+1, attemptHeaders[i], want)
63+
}
64+
}
65+
}

volcengine/request/request.go

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"strings"
1313
"time"
1414

15+
"github.com/google/uuid"
16+
1517
"github.com/volcengine/volcengine-go-sdk/volcengine"
1618
"github.com/volcengine/volcengine-go-sdk/volcengine/custom"
1719
"github.com/volcengine/volcengine-go-sdk/volcengine/response"
@@ -50,19 +52,24 @@ type Request struct {
5052
Handlers Handlers
5153

5254
Retryer
53-
AttemptTime time.Time
54-
Time time.Time
55-
Operation *Operation
56-
HTTPRequest *http.Request
57-
HTTPResponse *http.Response
58-
Body io.ReadSeeker
59-
BodyStart int64 // offset from beginning of Body that the request volcenginebody starts
60-
Params interface{}
61-
Error error
62-
Data interface{}
63-
RequestID string
64-
RetryCount int
65-
Retryable *bool
55+
AttemptTime time.Time
56+
Time time.Time
57+
Operation *Operation
58+
HTTPRequest *http.Request
59+
HTTPResponse *http.Response
60+
Body io.ReadSeeker
61+
BodyStart int64 // offset from beginning of Body that the request volcenginebody starts
62+
Params interface{}
63+
Error error
64+
Data interface{}
65+
RequestID string
66+
RetryCount int
67+
Retryable *bool
68+
// InvocationID identifies a single logical Send() call: it is generated
69+
// once in New() and stays the same across all retry attempts of that
70+
// call, letting the server tell attempts of the same request apart from
71+
// unrelated ones.
72+
InvocationID string
6673
RetryDelay time.Duration
6774
NotHoist bool
6875
SignedHeaderVals http.Header
@@ -136,18 +143,19 @@ func New(cfg volcengine.Config, clientInfo metadata.ClientInfo, handlers Handler
136143
SanitizeHostForHeader(httpReq)
137144

138145
r := &Request{
139-
Config: cfg,
140-
ClientInfo: clientInfo,
141-
Handlers: handlers.Copy(),
142-
Retryer: retryer,
143-
Time: time.Now(),
144-
ExpireTime: 0,
145-
Operation: operation,
146-
HTTPRequest: httpReq,
147-
Body: nil,
148-
Params: params,
149-
Error: err,
150-
Data: data,
146+
Config: cfg,
147+
ClientInfo: clientInfo,
148+
Handlers: handlers.Copy(),
149+
Retryer: retryer,
150+
Time: time.Now(),
151+
ExpireTime: 0,
152+
Operation: operation,
153+
HTTPRequest: httpReq,
154+
Body: nil,
155+
Params: params,
156+
Error: err,
157+
Data: data,
158+
InvocationID: uuid.New().String(),
151159
}
152160
r.SetBufferBody([]byte{})
153161

0 commit comments

Comments
 (0)