feat: add idempotent batch task submission and recovery examples
This commit is contained in:
107
sdk/go/batch_integration_test.go
Normal file
107
sdk/go/batch_integration_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package orbit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIntegrationTaskBatchSDK(t *testing.T) {
|
||||
api, _, _ := integrationWorkerClient(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
created, err := api.CreateQueue(ctx, workerTestID(t), CreateQueueRequest{Name: "batch." + workerTestID(t), MaxConcurrency: 2, RatePerSecond: 10, MaxUnfinishedTasks: 2})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q := created.Queue
|
||||
if _, err := api.ConfigureQueue(ctx, q.ID, workerTestID(t), ConfigureQueueRequest{ExpectedRevision: q.Revision, Reason: "batch acceptance regression", Paused: true, MaxConcurrency: 2, RatePerSecond: 10, MaxUnfinishedTasks: 2}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
input := SubmitTaskRequest{QueueID: q.ID, Handler: "file.convert", HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{"n":9007199254740993}`)}
|
||||
in := SubmitTasksRequest{Items: []SubmitTasksItem{{IdempotencyKey: workerTestID(t), Task: input}, {IdempotencyKey: workerTestID(t), Task: input}, {IdempotencyKey: workerTestID(t), Task: input}}}
|
||||
// 真实HTTP代理先完整读到数据库受理回执,再强制断开客户端连接,丢弃整批响应。
|
||||
target, _ := url.Parse(api.baseURL)
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
var dropped atomic.Bool
|
||||
var calls atomic.Int32
|
||||
proxy.ModifyResponse = func(r *http.Response) error {
|
||||
if r.Request.URL.Path != "/v1/tasks:batch" {
|
||||
return nil
|
||||
}
|
||||
calls.Add(1)
|
||||
if dropped.CompareAndSwap(false, true) {
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.StatusCode != 200 || !strings.Contains(string(raw), `"ACCEPTED"`) || !strings.Contains(string(raw), `"QUEUE_FULL"`) {
|
||||
t.Error("unexpected pre-drop server result", string(raw))
|
||||
}
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
conn, _, hijackErr := w.(http.Hijacker).Hijack()
|
||||
if hijackErr == nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
server := httptest.NewServer(proxy)
|
||||
defer server.Close()
|
||||
client, err := NewClient(server.URL, Options{Token: api.token})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := client.SubmitTasks(ctx, in); err == nil || !dropped.Load() || calls.Load() != 1 {
|
||||
t.Fatal("response loss not surfaced without silent retry", err)
|
||||
}
|
||||
// 原连接已关闭;用原键和原内容核对,前两项去重,满额项保持明确拒绝。
|
||||
out, err := client.SubmitTasks(ctx, in)
|
||||
if err != nil || len(out.Items) != 3 {
|
||||
t.Fatal(out, err)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if out.Items[i].Status != BatchAccepted || !out.Items[i].Receipt.Deduplicated {
|
||||
t.Fatal("lost response duplicated task", out)
|
||||
}
|
||||
}
|
||||
if out.Items[2].Status != BatchRejected || out.Items[2].Error.Code != "QUEUE_FULL" || out.Items[2].Error.RetryAfterMS != 1000 {
|
||||
t.Fatal(out)
|
||||
}
|
||||
page, err := api.ListTasks(ctx, ListTasksRequest{QueueID: q.ID})
|
||||
if err != nil || len(page.Items) != 2 {
|
||||
t.Fatal("duplicate durable rows", page, err)
|
||||
}
|
||||
single, err := api.SubmitTask(ctx, in.Items[0].IdempotencyKey, input)
|
||||
if err != nil || !single.Deduplicated || single.Task.ID != out.Items[0].Receipt.TaskID || string(single.Task.Payload) != `{"n":9007199254740993}` {
|
||||
t.Fatal("batch/single mismatch", err)
|
||||
}
|
||||
if _, err := api.CancelTask(ctx, single.Task.ID, workerTestID(t), CancelTaskRequest{ExpectedRevision: single.Task.Revision, Reason: "free slot for partial retry"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
suffix, err := api.SubmitTasks(ctx, SubmitTasksRequest{Items: in.Items[2:]})
|
||||
if err != nil || suffix.Items[0].Status != BatchAccepted || suffix.Items[0].Receipt.Deduplicated || suffix.Items[0].Index != 0 {
|
||||
t.Fatal(suffix, err)
|
||||
}
|
||||
for _, id := range []string{out.Items[1].Receipt.TaskID, suffix.Items[0].Receipt.TaskID} {
|
||||
current, err := api.GetTask(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := api.CancelTask(ctx, id, workerTestID(t), CancelTaskRequest{ExpectedRevision: current.Revision, Reason: "finish batch regression"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Log("lost real HTTP response: two committed tasks recovered by identity, no duplicates; backpressured suffix independently accepted after capacity released")
|
||||
}
|
||||
Reference in New Issue
Block a user