feat: expose result subscription HTTP and WebSocket APIs

This commit is contained in:
2026-09-17 19:57:55 +08:00
parent 1c3300cc5b
commit 8485a662b2
38 changed files with 3462 additions and 26 deletions

View File

@@ -0,0 +1,181 @@
package orbit
import (
"context"
"encoding/json"
"net/http"
"strings"
"testing"
"time"
"github.com/coder/websocket"
)
// 该测试用原始WSS帧驱动交付,用于验证管理SDK;不是待交付的订阅连接SDK替代品。
type managementEventFrame struct {
Version string `json:"protocol_version"`
Type string `json:"type"`
MessageID string `json:"message_id"`
SessionID string `json:"session_id,omitempty"`
ReplyTo string `json:"reply_to,omitempty"`
Body json.RawMessage `json:"body,omitempty"`
Error *struct {
Code string `json:"code"`
} `json:"error,omitempty"`
}
func TestIntegrationSubscriptionManagementSDK(t *testing.T) {
api, _, _ := integrationWorkerClient(t)
ctx, stop := context.WithTimeout(context.Background(), 45*time.Second)
defer stop()
queue, err := api.CreateQueue(ctx, workerTestID(t), CreateQueueRequest{Name: "subscription-" + workerTestID(t), MaxConcurrency: 1, RatePerSecond: 10})
if err != nil {
t.Fatal(err)
}
key := workerTestID(t)
in := CreateSubscriptionRequest{Name: "results-" + workerTestID(t), QueueID: queue.Queue.ID, MaxPending: 1, MaxAttempts: 1, Types: []string{"task.cancelled"}}
created, err := api.CreateSubscription(ctx, key, in)
if err != nil {
t.Fatal(err)
}
duplicate, err := api.CreateSubscription(ctx, key, in)
if err != nil || !duplicate.Deduplicated || duplicate.Subscription.ID != created.Subscription.ID {
t.Fatal("SDK subscription create replay", err)
}
sub := created.Subscription
current, err := api.GetSubscription(ctx, sub.ID)
if err != nil || current.ID != sub.ID || !current.Enabled {
t.Fatal("SDK subscription get", err)
}
page, err := api.ListSubscriptions(ctx, queue.Queue.ID, SubscriptionPageRequest{Limit: 1})
if err != nil || len(page.Items) != 1 || page.Items[0].ID != sub.ID {
t.Fatal("SDK subscription list", err)
}
paused, err := api.ConfigureSubscription(ctx, sub.ID, workerTestID(t), ConfigureSubscriptionRequest{ExpectedRevision: sub.Revision, Reason: "maintenance", Enabled: false})
if err != nil || paused.Subscription.Enabled {
t.Fatal("SDK pause", err)
}
resumed, err := api.ConfigureSubscription(ctx, sub.ID, workerTestID(t), ConfigureSubscriptionRequest{ExpectedRevision: paused.Subscription.Revision, Reason: "ready", Enabled: true})
if err != nil || !resumed.Subscription.Enabled {
t.Fatal("SDK resume", err)
}
task, err := api.SubmitTask(ctx, workerTestID(t), SubmitTaskRequest{QueueID: queue.Queue.ID, Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{}`)})
if err != nil {
t.Fatal(err)
}
if _, err = api.CancelTask(ctx, task.Task.ID, workerTestID(t), CancelTaskRequest{ExpectedRevision: task.Task.Revision, Reason: "create result"}); err != nil {
t.Fatal(err)
}
send := func(conn *websocket.Conn, session, kind string, body any) string {
t.Helper()
id := workerTestID(t)
raw, err := marshalWire(body)
if err != nil {
t.Fatal(err)
}
frame, err := marshalWire(managementEventFrame{Version: "1", Type: kind, MessageID: id, SessionID: session, Body: raw})
if err != nil {
t.Fatal(err)
}
if err = conn.Write(ctx, websocket.MessageText, frame); err != nil {
t.Fatal(err)
}
return id
}
read := func(conn *websocket.Conn) managementEventFrame {
t.Helper()
bounded, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_, raw, err := conn.Read(bounded)
if err != nil {
t.Fatal(err)
}
var frame managementEventFrame
if json.Unmarshal(raw, &frame) != nil || frame.Version != "1" || !validID(frame.MessageID) {
t.Fatal("invalid management test frame")
}
return frame
}
connect := func() (*websocket.Conn, string) {
t.Helper()
headers := http.Header{}
headers.Set("Authorization", "Bearer "+api.token)
endpoint := "ws" + strings.TrimPrefix(api.baseURL, "http") + "/v1/events/connect"
conn, _, err := websocket.Dial(ctx, endpoint, &websocket.DialOptions{HTTPClient: api.http, HTTPHeader: headers, Subprotocols: []string{"orbit.events.v1"}})
if err != nil {
t.Fatal("management test event dial failed")
}
t.Cleanup(func() { conn.CloseNow() })
conn.SetReadLimit(512 << 10)
msg := send(conn, "", "events.hello", map[string]any{"subscription_id": sub.ID, "capacity": 1})
welcome := read(conn)
if welcome.Type != "events.welcome" || welcome.ReplyTo != msg || !validID(welcome.SessionID) {
t.Fatal("management test welcome missing")
}
return conn, welcome.SessionID
}
type grant struct {
Delivery EventDelivery `json:"delivery"`
Event ResultEvent `json:"event"`
Secret string `json:"lease_secret"`
}
a, _ := connect()
frame := read(a)
var first grant
if frame.Type != "event.deliver" || json.Unmarshal(frame.Body, &first) != nil || first.Event.Snapshot.TaskID != task.Task.ID {
t.Fatal("management test delivery missing")
}
a.CloseNow()
b, session := connect()
// 使用实际30秒租约,不改数据库时钟或手工更新租约时间。
var expired EventDelivery
for {
deliveries, err := api.ListEventDeliveries(ctx, sub.ID, DeliveryPageRequest{State: DeliveryExpired, Limit: 1})
if err != nil {
t.Fatal(err)
}
if len(deliveries.Items) == 1 {
expired = deliveries.Items[0]
break
}
select {
case <-ctx.Done():
t.Fatal("delivery did not expire through real WSS recovery")
case <-time.After(150 * time.Millisecond):
}
}
detail, err := api.GetEventDelivery(ctx, sub.ID, expired.ID)
if err != nil || detail.Event.ID != first.Event.ID || detail.Delivery.Attempts != 1 || detail.Event.Snapshot.State != TaskCancelled {
t.Fatal("SDK delivery detail", err)
}
retryKey := workerTestID(t)
retry := RetryDeliveryRequest{ExpectedRevision: expired.Revision, Reason: "receiver fixed", AdditionalAttempts: 1}
restored, err := api.RetryEventDelivery(ctx, sub.ID, expired.ID, retryKey, retry)
if err != nil || restored.Delivery.Attempts != 1 || restored.Delivery.AttemptBudget != 2 {
t.Fatal("SDK retry reset history", err)
}
replay, err := api.RetryEventDelivery(ctx, sub.ID, expired.ID, retryKey, retry)
if err != nil || !replay.Deduplicated || replay.Delivery.Revision != restored.Delivery.Revision {
t.Fatal("SDK retry replay", err)
}
frame = read(b)
var second grant
if frame.Type != "event.deliver" || json.Unmarshal(frame.Body, &second) != nil || second.Event.ID != first.Event.ID || second.Delivery.LeaseEpoch != 2 {
t.Fatal("manual retry failed to deliver")
}
ackBody := map[string]any{"delivery_id": second.Delivery.ID, "epoch": "2", "lease_secret": second.Secret}
msg := send(b, session, "event.ack", ackBody)
ack := read(b)
if ack.Type != "ack" || ack.ReplyTo != msg {
t.Fatal("management test ACK failed")
}
delivered, err := api.ListEventDeliveries(ctx, sub.ID, DeliveryPageRequest{State: DeliveryAcked})
if err != nil || len(delivered.Items) != 1 || delivered.Items[0].ID != expired.ID {
t.Fatal("SDK acknowledged history", err)
}
current, err = api.GetSubscription(ctx, sub.ID)
if err != nil || current.PendingCount != 0 || current.Revision != resumed.Subscription.Revision {
t.Fatal("SDK pending count or config version", err)
}
t.Log("management SDK exercised create/get/list/control/detail/expired retry with a real 30-second delivery lease")
}