feat: add idempotent batch task submission and recovery examples

This commit is contained in:
2026-09-17 21:21:18 +08:00
parent a8a8dcfe1e
commit 571107f96d
27 changed files with 1375 additions and 24 deletions

75
sdk/go/batch_test.go Normal file
View File

@@ -0,0 +1,75 @@
package orbit
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
)
func TestBatchSDKWireValidationAndNoAutomaticRetry(t *testing.T) {
id := strings.Repeat("1", 32)
var calls atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
if r.URL.Path != "/v1/tasks:batch" || r.Method != "POST" || r.Header.Get("Authorization") != "Bearer test-token" || r.Header.Get("Idempotency-Key") != "" {
t.Error("invalid batch transport")
}
raw, _ := io.ReadAll(r.Body)
if !strings.Contains(string(raw), `"idempotency_key":"one"`) || !strings.Contains(string(raw), `9007199254740993`) {
t.Error("identity/precision lost")
}
if calls.Load() == 1 {
w.WriteHeader(503)
fmt.Fprintf(w, `{"request_id":%q,"error":{"code":"UNAVAILABLE","message":"retry original identities"}}`, id)
return
}
fmt.Fprintf(w, `{"request_id":%q,"data":{"items":[{"index":0,"status":"ACCEPTED","receipt":{"task_id":%q,"queue_id":%q,"revision":"9007199254740993","created_at":"2026-09-17T00:00:00Z","deduplicated":true}}]}}`, id, id, id)
}))
defer srv.Close()
c, _ := NewClient(srv.URL, Options{Token: "test-token"})
in := SubmitTasksRequest{Items: []SubmitTasksItem{{IdempotencyKey: "one", Task: SubmitTaskRequest{QueueID: id, Payload: json.RawMessage(`{"n":9007199254740993}`)}}}}
if _, err := c.SubmitTasks(context.Background(), in); err == nil || calls.Load() != 1 {
t.Fatal("request silently retried", err)
}
out, err := c.SubmitTasks(context.Background(), in)
if err != nil || out.Items[0].Receipt.Revision != 9007199254740993 || !out.Items[0].Receipt.Deduplicated {
t.Fatal(out, err)
}
for _, bad := range []SubmitTasksRequest{{}, {Items: make([]SubmitTasksItem, 33)}, {Items: []SubmitTasksItem{in.Items[0], in.Items[0]}}, {Items: []SubmitTasksItem{{IdempotencyKey: " "}}}} {
if _, err := c.SubmitTasks(context.Background(), bad); err == nil {
t.Fatal("accepted invalid batch")
}
}
in.Items[0].Task.Payload = json.RawMessage(`"` + strings.Repeat("x", 512<<10) + `"`)
if _, err := c.SubmitTasks(context.Background(), in); err == nil || calls.Load() != 2 {
t.Fatal("unbounded batch reached network")
}
}
func TestBatchSDKRejectsIncompleteAndContradictoryReceipts(t *testing.T) {
id := strings.Repeat("1", 32)
for _, data := range []string{
`{"items":[]}`,
`{"items":[{"index":1,"status":"REJECTED","error":{"code":"INVALID_ARGUMENT","message":"bad"}}]}`,
`{"items":[{"index":0,"status":"UNKNOWN","error":{"code":"INVALID_ARGUMENT","message":"bad"}}]}`,
`{"items":[{"index":0,"status":"REJECTED","error":{"code":"UNAVAILABLE","message":"unknown"}}]}`,
`{"items":[{"index":0,"status":"NOT_ATTEMPTED","error":{"code":"UNAVAILABLE","message":"bad"}}]}`,
`{"items":[{"index":0,"status":"ACCEPTED"}]}`,
`{"items":[{"index":0,"status":"REJECTED","receipt":{},"error":{"code":"QUEUE_FULL","message":"full"}}]}`,
`{"items":[{"index":0,"status":"NEW_STATUS"}]}`,
} {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"request_id":%q,"data":%s}`, id, data) }))
c, _ := NewClient(srv.URL, Options{Token: "test-token"})
out, err := c.SubmitTasks(context.Background(), SubmitTasksRequest{Items: []SubmitTasksItem{{IdempotencyKey: "one", Task: SubmitTaskRequest{QueueID: id}}}})
srv.Close()
if err == nil || len(out.Items) != 0 {
t.Fatal("malformed response exposed as success", out, data)
}
}
}