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

@@ -67,7 +67,7 @@ func TestIntegrationApplicationResourcesUpgrade(t *testing.T) {
f.exec(`DROP TABLE orbit_application_resources`)
f.exec(`DROP TABLE orbit_result_events,orbit_event_streams`)
f.exec(`ALTER TABLE orbit_subscriptions DROP CHECK ck_subscription_window, DROP COLUMN queue_id, DROP COLUMN cursor_sequence, DROP COLUMN pending_count, DROP COLUMN max_pending, DROP COLUMN max_attempts`)
f.exec(`ALTER TABLE orbit_deliveries DROP INDEX ix_delivery_subscription_due, DROP INDEX ix_delivery_subscription_lease, DROP CHECK ck_delivery_budget, DROP COLUMN attempt_budget, DROP COLUMN token_digest`)
f.exec(`ALTER TABLE orbit_deliveries DROP INDEX ix_delivery_history, DROP INDEX ix_delivery_state_history, DROP INDEX ix_delivery_subscription_due, DROP INDEX ix_delivery_subscription_lease, DROP CHECK ck_delivery_budget, DROP COLUMN attempt_budget, DROP COLUMN token_digest`)
f.exec(`ALTER TABLE pulse_tasks DROP INDEX ix_task_priority_dispatch, DROP CHECK ck_task_priority, DROP COLUMN dispatch_at`)
f.exec(`ALTER TABLE pulse_tasks DROP CHECK ck_task_concurrency_key, DROP COLUMN concurrency_key, DROP COLUMN concurrency_key_digest`)
f.exec(`ALTER TABLE pulse_attempts DROP COLUMN concurrency_token`)

View File

@@ -98,12 +98,12 @@ type storedDelivery struct {
func readDelivery(ctx context.Context, tx *sql.Tx, ns, sub, id core.ID, lock bool) (storedDelivery, error) {
var out storedDelivery
q := `SELECT id,subscription_id,event_id,revision,state,available_at,lease_epoch,lease_until,attempts,attempt_budget,acknowledged_at,token_digest FROM orbit_deliveries WHERE namespace_id=? AND subscription_id=? AND id=?`
q := `SELECT id,subscription_id,event_id,revision,state,available_at,lease_epoch,lease_until,attempts,attempt_budget,acknowledged_at,token_digest,created_at FROM orbit_deliveries WHERE namespace_id=? AND subscription_id=? AND id=?`
if lock {
q += ` FOR UPDATE`
}
v := &out.view
err := tx.QueryRowContext(ctx, q, ns, sub, id).Scan(&v.ID, &v.SubscriptionID, &v.EventID, &v.Revision, &v.State, &v.AvailableAt, &v.LeaseEpoch, &v.LeaseUntil, &v.Attempts, &v.AttemptBudget, &v.AcknowledgedAt, &out.digest)
err := tx.QueryRowContext(ctx, q, ns, sub, id).Scan(&v.ID, &v.SubscriptionID, &v.EventID, &v.Revision, &v.State, &v.AvailableAt, &v.LeaseEpoch, &v.LeaseUntil, &v.Attempts, &v.AttemptBudget, &v.AcknowledgedAt, &out.digest, &v.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {
return out, fault.NotFound
}

View File

@@ -0,0 +1,179 @@
package mysql
import (
"bytes"
"context"
"database/sql"
"encoding/base64"
"encoding/json"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
"git.yuntuo.hk/k2safe/orbit/internal/jsonvalue"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/platform/events"
platform "git.yuntuo.hk/k2safe/orbit/platform/model"
)
type eventCursor struct {
Version int `json:"v"`
Kind string `json:"k"`
Namespace core.ID `json:"n"`
Credential core.ID `json:"c"`
Parent core.ID `json:"p"`
State platform.DeliveryState `json:"s"`
At time.Time `json:"t"`
ID core.ID `json:"i"`
}
func parseEventCursor(raw string, t auth.Token, parent core.ID, kind string, state platform.DeliveryState) (eventCursor, error) {
var c eventCursor
b, err := base64.RawURLEncoding.Strict().DecodeString(raw)
if err != nil || len(b) > 1024 {
return c, fault.Invalid("invalid event cursor")
}
if _, err = jsonvalue.Canonical(b); err != nil {
return c, fault.Invalid("invalid event cursor")
}
var fields map[string]json.RawMessage
if json.Unmarshal(b, &fields) != nil || len(fields) != 8 {
return c, fault.Invalid("invalid event cursor")
}
for _, k := range []string{"v", "k", "n", "c", "p", "s", "t", "i"} {
if _, ok := fields[k]; !ok {
return c, fault.Invalid("invalid event cursor")
}
}
d := json.NewDecoder(bytes.NewReader(b))
d.DisallowUnknownFields()
if d.Decode(&c) != nil || c.Version != 1 || c.Kind != kind || c.Namespace != t.NamespaceID || c.Credential != t.CredentialID || c.Parent != parent || c.State != state || c.ID.Validate() != nil || c.At.Year() < 2000 || c.At.Year() > 9998 || c.At.Nanosecond()%1000 != 0 {
return c, fault.Invalid("cursor does not match event query")
}
return c, nil
}
func makeEventCursor(t auth.Token, parent core.ID, kind string, state platform.DeliveryState, at time.Time, id core.ID) (string, error) {
b, err := json.Marshal(eventCursor{1, kind, t.NamespaceID, t.CredentialID, parent, state, at, id})
return base64.RawURLEncoding.EncodeToString(b), err
}
func (s *EventStore) AuthorizeEvents(ctx context.Context, t auth.Token) error {
return s.transaction(ctx, t, "events:consume", func(*sql.Tx, auth.Permissions) error { return nil })
}
func (s *EventStore) BindConsumer(ctx context.Context, t auth.Token, id core.ID) (events.ConsumerBinding, error) {
var out events.ConsumerBinding
err := s.transaction(ctx, t, "events:consume", func(tx *sql.Tx, p auth.Permissions) error {
sub, err := readSubscription(ctx, tx, t, p, id, false)
if err != nil {
return err
}
out.SubscriptionID = sub.ID
out.QueueID = sub.QueueID
out.Enabled = sub.Enabled
return tx.QueryRowContext(ctx, `SELECT application_id,UTC_TIMESTAMP(6) FROM orbit_namespaces WHERE id=?`, t.NamespaceID).Scan(&out.ApplicationID, &out.ServerTime)
})
return out, err
}
func (s *EventStore) ListSubscriptions(ctx context.Context, t auth.Token, queue core.ID, in events.PageInput) (events.SubscriptionPage, error) {
out := events.SubscriptionPage{Items: []events.Subscription{}}
err := s.transaction(ctx, t, "events:read", func(tx *sql.Tx, p auth.Permissions) error {
if !p.AllowsQueue(queue) {
return fault.Forbidden
}
q := `SELECT ` + subscriptionColumns + ` FROM orbit_subscriptions WHERE namespace_id=? AND credential_id=? AND queue_id=?`
args := []any{t.NamespaceID, t.CredentialID, queue}
if in.Cursor != "" {
c, err := parseEventCursor(in.Cursor, t, queue, "subscriptions", "")
if err != nil {
return err
}
q += ` AND (created_at<? OR (created_at=? AND id<?))`
args = append(args, c.At, c.At, c.ID)
}
q += ` ORDER BY created_at DESC,id DESC LIMIT ?`
args = append(args, in.Limit+1)
rows, err := tx.QueryContext(ctx, q, args...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
v, err := scanSubscription(rows)
if err != nil {
return err
}
out.Items = append(out.Items, v)
}
if err = rows.Err(); err != nil {
return err
}
if len(out.Items) > in.Limit {
out.Items = out.Items[:in.Limit]
last := out.Items[len(out.Items)-1]
out.NextCursor, err = makeEventCursor(t, queue, "subscriptions", "", last.CreatedAt, last.ID)
}
return err
})
return out, err
}
func (s *EventStore) ListDeliveries(ctx context.Context, t auth.Token, id core.ID, in events.PageInput) (events.DeliveryPage, error) {
out := events.DeliveryPage{Items: []events.Delivery{}}
err := s.transaction(ctx, t, "events:read", func(tx *sql.Tx, p auth.Permissions) error {
if _, err := readSubscription(ctx, tx, t, p, id, false); err != nil {
return err
}
q := `SELECT d.id,d.subscription_id,d.event_id,r.sequence,d.revision,d.state,d.available_at,d.lease_epoch,d.lease_until,d.attempts,d.attempt_budget,d.acknowledged_at,d.created_at FROM orbit_deliveries d LEFT JOIN orbit_result_events r ON r.namespace_id=d.namespace_id AND r.id=d.event_id WHERE d.namespace_id=? AND d.subscription_id=?`
args := []any{t.NamespaceID, id}
if in.State != "" {
q += ` AND d.state=?`
args = append(args, in.State)
}
if in.Cursor != "" {
c, err := parseEventCursor(in.Cursor, t, id, "deliveries", in.State)
if err != nil {
return err
}
q += ` AND (d.created_at<? OR (d.created_at=? AND d.id<?))`
args = append(args, c.At, c.At, c.ID)
}
q += ` ORDER BY d.created_at DESC,d.id DESC LIMIT ?`
args = append(args, in.Limit+1)
rows, err := tx.QueryContext(ctx, q, args...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var v events.Delivery
if err = rows.Scan(&v.ID, &v.SubscriptionID, &v.EventID, &v.Sequence, &v.Revision, &v.State, &v.AvailableAt, &v.LeaseEpoch, &v.LeaseUntil, &v.Attempts, &v.AttemptBudget, &v.AcknowledgedAt, &v.CreatedAt); err != nil {
return err
}
out.Items = append(out.Items, v)
}
if err = rows.Err(); err != nil {
return err
}
if len(out.Items) > in.Limit {
out.Items = out.Items[:in.Limit]
last := out.Items[len(out.Items)-1]
out.NextCursor, err = makeEventCursor(t, id, "deliveries", in.State, last.CreatedAt, last.ID)
}
return err
})
return out, err
}
func (s *EventStore) GetDeliveryDetail(ctx context.Context, t auth.Token, sub, id core.ID) (events.DeliveryDetail, error) {
var out events.DeliveryDetail
err := s.transaction(ctx, t, "events:read", func(tx *sql.Tx, p auth.Permissions) error {
if _, err := readSubscription(ctx, tx, t, p, sub, false); err != nil {
return err
}
d, err := readDelivery(ctx, tx, t.NamespaceID, sub, id, false)
if err != nil {
return err
}
out.Delivery = d.view
out.Event, err = readResultEvent(ctx, tx, t.NamespaceID, d.view.EventID)
return err
})
return out, err
}

View File

@@ -27,13 +27,11 @@ var _ events.Store = (*EventStore)(nil)
const subscriptionColumns = `id,queue_id,credential_id,name,revision,enabled,filter,cursor_sequence,pending_count,max_pending,max_attempts,created_at,updated_at`
func readSubscription(ctx context.Context, tx *sql.Tx, t auth.Token, p auth.Permissions, id core.ID, lock bool) (events.Subscription, error) {
var out events.Subscription
var raw []byte
q := `SELECT ` + subscriptionColumns + ` FROM orbit_subscriptions WHERE namespace_id=? AND id=? AND credential_id=? AND queue_id IS NOT NULL`
if lock {
q += ` FOR UPDATE`
}
err := tx.QueryRowContext(ctx, q, t.NamespaceID, id, t.CredentialID).Scan(&out.ID, &out.QueueID, &out.CredentialID, &out.Name, &out.Revision, &out.Enabled, &raw, &out.CursorSequence, &out.PendingCount, &out.MaxPending, &out.MaxAttempts, &out.CreatedAt, &out.UpdatedAt)
out, err := scanSubscription(tx.QueryRowContext(ctx, q, t.NamespaceID, id, t.CredentialID))
if errors.Is(err, sql.ErrNoRows) {
return out, fault.NotFound
}
@@ -43,10 +41,17 @@ func readSubscription(ctx context.Context, tx *sql.Tx, t auth.Token, p auth.Perm
if !p.AllowsQueue(out.QueueID) {
return events.Subscription{}, fault.NotFound
}
if err = json.Unmarshal(raw, &out.Types); err != nil {
return out, nil
}
func scanSubscription(row scanner) (events.Subscription, error) {
var out events.Subscription
var raw []byte
err := row.Scan(&out.ID, &out.QueueID, &out.CredentialID, &out.Name, &out.Revision, &out.Enabled, &raw, &out.CursorSequence, &out.PendingCount, &out.MaxPending, &out.MaxAttempts, &out.CreatedAt, &out.UpdatedAt)
if err != nil {
return out, err
}
return out, nil
err = json.Unmarshal(raw, &out.Types)
return out, err
}
func (s *EventStore) CreateSubscription(ctx context.Context, t auth.Token, key string, in events.CreateInput) (events.SubscriptionResult, error) {

View File

@@ -0,0 +1,502 @@
package mysql
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/jsonwire"
"git.yuntuo.hk/k2safe/orbit/internal/testcontract"
"git.yuntuo.hk/k2safe/orbit/internal/transport/httpserver"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/events"
platform "git.yuntuo.hk/k2safe/orbit/platform/model"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
"git.yuntuo.hk/k2safe/orbit/pulse/worker"
"github.com/coder/websocket"
)
func startEventServer(t *testing.T, ctx context.Context, db *sql.DB) (*httptest.Server, *httpserver.EventHub) {
t.Helper()
svc := events.New(NewEventStore(db))
hub := httpserver.NewEventHub(ctx, svc)
server := httpserver.New("", func(context.Context) error { return nil }, service.New(NewTaskStore(db)))
httpserver.RegisterEventSubscriptions(server, svc)
httpserver.RegisterEventConsumers(server, hub)
tls := httptest.NewTLSServer(server.Handler)
t.Cleanup(func() {
bounded, stop := context.WithTimeout(context.Background(), 3*time.Second)
defer stop()
if err := hub.Shutdown(bounded); err != nil {
t.Error(err)
}
tls.Close()
})
return tls, hub
}
func eventHTTP(t *testing.T, ctx context.Context, server *httptest.Server, method, path, token, key string, body any, status int) json.RawMessage {
t.Helper()
var raw []byte
var err error
switch b := body.(type) {
case nil:
case string:
raw = []byte(b)
default:
raw, err = jsonwire.Marshal(body)
if err != nil {
t.Fatal(err)
}
}
req, err := http.NewRequestWithContext(ctx, method, server.URL+path, bytes.NewReader(raw))
if err != nil {
t.Fatal(err)
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
if key != "" {
req.Header.Set("Idempotency-Key", key)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := server.Client().Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
t.Fatal(err)
}
var envelope struct {
RequestID core.ID `json:"request_id"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
} `json:"error"`
}
if err = json.Unmarshal(data, &envelope); err != nil {
t.Fatalf("invalid event HTTP response: status %d", resp.StatusCode)
}
if resp.StatusCode != status {
code := ""
if envelope.Error != nil {
code = envelope.Error.Code
}
t.Fatalf("expected HTTP %d, got %d/%s", status, resp.StatusCode, code)
}
if envelope.RequestID.Validate() != nil || resp.Header.Get("Cache-Control") != "no-store" {
t.Fatal("missing event API envelope metadata")
}
if status >= 400 {
if envelope.Error == nil || len(envelope.Data) != 0 {
t.Fatal("invalid event error response")
}
return data
}
if envelope.Error != nil {
return nil
}
return envelope.Data
}
type testEventSocket struct {
t *testing.T
ctx context.Context
conn *websocket.Conn
session core.ID
pending []testWorkerFrame
}
func connectTestEvents(t *testing.T, ctx context.Context, server *httptest.Server, f *eventFixture, sub core.ID, capacity uint32) *testEventSocket {
t.Helper()
headers := http.Header{}
headers.Set("Authorization", "Bearer "+f.resources.Token)
conn, resp, err := websocket.Dial(ctx, strings.Replace(server.URL, "https://", "wss://", 1)+"/v1/events/connect", &websocket.DialOptions{HTTPClient: server.Client(), HTTPHeader: headers, Subprotocols: []string{httpserver.EventSubprotocol}})
if err != nil {
status := 0
if resp != nil {
status = resp.StatusCode
}
t.Fatalf("event dial failed HTTP %d", status)
}
t.Cleanup(func() { conn.CloseNow() })
conn.SetReadLimit(512 << 10)
c := &testEventSocket{t: t, ctx: ctx, conn: conn}
hello := c.send("events.hello", map[string]any{"subscription_id": sub, "capacity": capacity}, "")
frame := c.read()
if frame.Type != "events.welcome" || frame.ReplyTo != hello || frame.SessionID.Validate() != nil {
t.Fatal("missing event welcome")
}
var welcome struct {
SessionID core.ID `json:"session_id"`
SubscriptionID core.ID `json:"subscription_id"`
Capacity uint32 `json:"capacity"`
LeaseDurationMS int `json:"lease_duration_ms"`
ServerTime time.Time `json:"server_time"`
}
if json.Unmarshal(frame.Body, &welcome) != nil || welcome.SessionID != frame.SessionID || welcome.SubscriptionID != sub || welcome.Capacity != capacity || welcome.LeaseDurationMS != 30000 || welcome.ServerTime.IsZero() {
t.Fatal("invalid welcome binding")
}
c.session = frame.SessionID
return c
}
func (c *testEventSocket) send(kind string, body any, id core.ID) core.ID {
c.t.Helper()
if id == "" {
id = executionID(c.t)
}
payload, err := jsonwire.Marshal(body)
if err != nil {
c.t.Fatal(err)
}
raw, err := jsonwire.Marshal(testWorkerFrame{Version: "1", Type: kind, MessageID: id, SessionID: c.session, Body: payload})
if err != nil {
c.t.Fatal(err)
}
testcontract.EventFrame(c.t, "client", raw)
ctx, stop := context.WithTimeout(c.ctx, 5*time.Second)
defer stop()
if err = c.conn.Write(ctx, websocket.MessageText, raw); err != nil {
c.t.Fatal(err)
}
return id
}
func (c *testEventSocket) read() testWorkerFrame {
c.t.Helper()
ctx, stop := context.WithTimeout(c.ctx, 5*time.Second)
defer stop()
kind, raw, err := c.conn.Read(ctx)
if err != nil {
c.t.Fatal(err)
}
testcontract.EventFrame(c.t, "server", raw)
var frame testWorkerFrame
if kind != websocket.MessageText || json.Unmarshal(raw, &frame) != nil {
c.t.Fatal("invalid event frame")
}
if c.session != "" && frame.SessionID != c.session {
c.t.Fatal("event response changed session")
}
return frame
}
func (c *testEventSocket) reply(id core.ID, code string) json.RawMessage {
c.t.Helper()
for {
frame := c.read()
if frame.Type == "event.deliver" {
c.pending = append(c.pending, frame)
continue
}
if frame.ReplyTo != id {
c.t.Fatal("uncorrelated event reply")
}
if code != "" {
if frame.Type != "error" || frame.Error == nil || frame.Error.Code != code {
c.t.Fatal("unexpected event error")
}
} else if frame.Type != "ack" || frame.Error != nil {
c.t.Fatal("expected event acknowledgement")
}
return frame.Body
}
}
func (c *testEventSocket) grant() *events.Grant {
c.t.Helper()
var frame testWorkerFrame
if len(c.pending) > 0 {
frame = c.pending[0]
c.pending = c.pending[1:]
} else {
frame = c.read()
}
if frame.Type != "event.deliver" {
c.t.Fatalf("expected event delivery, got %s", frame.Type)
}
var g events.Grant
if json.Unmarshal(frame.Body, &g) != nil || g.Delivery.EventID != g.Event.ID || g.Delivery.Sequence != g.Event.Sequence || g.Delivery.State != platform.DeliveryLeased {
c.t.Fatal("invalid delivered event")
}
return &g
}
func eventProof(g *events.Grant) events.AckInput {
return events.AckInput{DeliveryID: g.Delivery.ID, Epoch: g.Delivery.LeaseEpoch, LeaseSecret: g.LeaseSecret}
}
func TestIntegrationEventHTTPAndWebSocket(t *testing.T) {
dsn := os.Getenv("ORBIT_TEST_MYSQL_DSN")
if dsn == "" {
t.Skip("requires disposable MySQL")
}
ctx, stop := context.WithTimeout(context.Background(), 90*time.Second)
defer stop()
db, err := Open(ctx, dsn, 20, 5)
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err = Migrate(ctx, db); err != nil {
t.Fatal(err)
}
serverA, hubA := startEventServer(t, ctx, db)
serverB, _ := startEventServer(t, ctx, db)
t.Run("HTTP_management_required_fields_and_scoped_pagination", func(t *testing.T) {
f := newEventFixture(t, ctx, db)
other := newEventFixture(t, ctx, db)
in := events.CreateInput{Name: "managed-results", QueueID: f.resources.QueueID, MaxAttempts: 1}
raw := eventHTTP(t, ctx, serverA, "POST", "/v1/subscriptions", f.resources.Token, "create", in, 201)
var created events.SubscriptionResult
if json.Unmarshal(raw, &created) != nil {
t.Fatal("invalid subscription receipt")
}
id := created.Subscription.ID
raw = eventHTTP(t, ctx, serverA, "POST", "/v1/subscriptions", f.resources.Token, "create", in, 200)
var duplicate events.SubscriptionResult
if json.Unmarshal(raw, &duplicate) != nil || !duplicate.Deduplicated || duplicate.Subscription.ID != id {
t.Fatal("idempotency changed through HTTP")
}
eventHTTP(t, ctx, serverA, "GET", "/v1/subscriptions/"+string(id), other.resources.Token, "", nil, 404)
eventHTTP(t, ctx, serverA, "POST", "/v1/subscriptions/"+string(id)+":configure", f.resources.Token, "invalid-control", `{"expected_revision":"1","reason":"ambiguous"}`, 400)
if !f.subscription(id).Enabled {
t.Fatal("missing enabled paused subscription")
}
eventHTTP(t, ctx, serverA, "POST", "/v1/subscriptions/"+string(id)+":configure", f.resources.Token, "pause", events.ControlInput{ExpectedRevision: 1, Enabled: false, Reason: "maintenance"}, 200)
eventHTTP(t, ctx, serverA, "POST", "/v1/subscriptions/"+string(id)+":configure", f.resources.Token, "resume", events.ControlInput{ExpectedRevision: 2, Enabled: true, Reason: "ready"}, 200)
for _, bad := range []string{`"00"`, `"+1"`, `1`, `false`, `"18446744073709551616"`} {
eventHTTP(t, ctx, serverA, "POST", "/v1/subscriptions", f.resources.Token, "invalid-sequence", `{"name":"invalid","queue_id":"`+string(f.resources.QueueID)+`","from_sequence":`+bad+`}`, 400)
}
for _, path := range []string{"/v1/subscriptions?queue_id=" + string(f.resources.QueueID) + "&limit=01", "/v1/subscriptions?queue_id=" + string(f.resources.QueueID) + "&limit=101", "/v1/subscriptions?queue_id=" + string(f.resources.QueueID) + "&cursor=a&cursor=b", "/v1/subscriptions/" + string(id) + "?unknown=1"} {
eventHTTP(t, ctx, serverA, "GET", path, f.resources.Token, "", nil, 400)
}
f.create(events.CreateInput{})
f.create(events.CreateInput{})
path := "/v1/subscriptions?queue_id=" + string(f.resources.QueueID) + "&limit=1"
var page events.SubscriptionPage
raw = eventHTTP(t, ctx, serverA, "GET", path, f.resources.Token, "", nil, 200)
if json.Unmarshal(raw, &page) != nil || len(page.Items) != 1 || page.NextCursor == "" {
t.Fatal("missing subscription page")
}
first := page.Items[0].ID
raw = eventHTTP(t, ctx, serverA, "GET", path+"&cursor="+url.QueryEscape(page.NextCursor), f.resources.Token, "", nil, 200)
if json.Unmarshal(raw, &page) != nil || page.Items[0].ID == first {
t.Fatal("subscription pagination repeated row")
}
eventHTTP(t, ctx, serverA, "GET", "/v1/subscriptions?queue_id="+string(other.resources.QueueID)+"&cursor="+url.QueryEscape(page.NextCursor), f.resources.Token, "", nil, 400)
for range 3 {
f.cancelResult()
}
firstGrant := f.grant(id)
f.ack(id, firstGrant)
second := f.grant(id)
f.expireDelivery(second)
third := f.grant(id)
path = "/v1/subscriptions/" + string(id) + "/deliveries"
var deliveries events.DeliveryPage
raw = eventHTTP(t, ctx, serverA, "GET", path+"?limit=1", f.resources.Token, "", nil, 200)
if json.Unmarshal(raw, &deliveries) != nil || len(deliveries.Items) != 1 || deliveries.NextCursor == "" {
t.Fatal("missing delivery page")
}
eventHTTP(t, ctx, serverA, "GET", path+"?state=EXPIRED&cursor="+url.QueryEscape(deliveries.NextCursor), f.resources.Token, "", nil, 400)
nextRaw := eventHTTP(t, ctx, serverA, "GET", path+"?limit=1&cursor="+url.QueryEscape(deliveries.NextCursor), f.resources.Token, "", nil, 200)
var nextPage events.DeliveryPage
if json.Unmarshal(nextRaw, &nextPage) != nil || len(nextPage.Items) != 1 || nextPage.Items[0].ID == deliveries.Items[0].ID {
t.Fatal("delivery page repeated a row")
}
eventHTTP(t, ctx, serverA, "GET", path+"/"+string(second.Delivery.ID), other.resources.Token, "", nil, 404)
wrongSub := f.create(events.CreateInput{})
eventHTTP(t, ctx, serverA, "GET", "/v1/subscriptions/"+string(wrongSub.ID)+"/deliveries/"+string(second.Delivery.ID), f.resources.Token, "", nil, 404)
raw = eventHTTP(t, ctx, serverA, "GET", path+"?state=EXPIRED", f.resources.Token, "", nil, 200)
if json.Unmarshal(raw, &deliveries) != nil || len(deliveries.Items) != 1 || deliveries.Items[0].ID != second.Delivery.ID {
t.Fatal("expired delivery not queryable")
}
expired := deliveries.Items[0]
raw = eventHTTP(t, ctx, serverA, "GET", path+"/"+string(expired.ID), f.resources.Token, "", nil, 200)
var detail events.DeliveryDetail
if json.Unmarshal(raw, &detail) != nil || detail.Event.ID != second.Event.ID || strings.Contains(string(raw), second.LeaseSecret) || strings.Contains(string(raw), "token_digest") {
t.Fatal("unsafe delivery detail")
}
retry := events.RetryInput{ExpectedRevision: expired.Revision, Reason: "consumer fixed", AdditionalAttempts: 1}
eventHTTP(t, ctx, serverA, "POST", path+"/"+string(expired.ID)+":retry", f.resources.Token, "retry", retry, 200)
eventHTTP(t, ctx, serverA, "POST", path+"/"+string(expired.ID)+":retry", f.resources.Token, "retry", retry, 200)
f.ack(id, third)
})
t.Run("handshake_authority_protocol_origin_and_URL", func(t *testing.T) {
f := newEventFixture(t, ctx, db)
plain := newExecutionFixture(t, ctx, db)
for _, c := range []struct {
token, protocol, path, origin string
status int
}{
{"", httpserver.EventSubprotocol, "/v1/events/connect", "", 401},
{plain.resources.Token, httpserver.EventSubprotocol, "/v1/events/connect", "", 403},
{f.resources.Token, "pulse.worker.v1", "/v1/events/connect", "", 426},
{f.resources.Token, httpserver.EventSubprotocol, "/v1/events/connect?token=forbidden", "", 400},
{f.resources.Token, httpserver.EventSubprotocol, "/v1/events/connect", "https://foreign.example", 403},
} {
headers := http.Header{}
if c.token != "" {
headers.Set("Authorization", "Bearer "+c.token)
}
if c.origin != "" {
headers.Set("Origin", c.origin)
}
conn, resp, err := websocket.Dial(ctx, strings.Replace(serverA.URL, "https://", "wss://", 1)+c.path, &websocket.DialOptions{HTTPClient: serverA.Client(), HTTPHeader: headers, Subprotocols: []string{c.protocol}})
if conn != nil {
conn.CloseNow()
}
if err == nil || resp == nil || resp.StatusCode != c.status {
t.Fatalf("event handshake expected HTTP %d", c.status)
}
}
})
t.Run("WSS_capacity_ack_receipt_and_result_snapshot", func(t *testing.T) {
f := newEventFixture(t, ctx, db)
sub := f.create(events.CreateInput{})
task := f.submit(1)
ref := retryStart(t, f.executionFixture)
result := json.RawMessage(`{"n":9007199254740993,"text":"` + strings.Repeat("<&>", 80000) + `"}`)
if _, err := f.a.Finish(ctx, f.token, f.binding, executionID(t), worker.FinishInput{Ref: ref, Outcome: "SUCCEEDED", Result: result}); err != nil {
t.Fatal(err)
}
f.cancelResult()
c := connectTestEvents(t, ctx, serverA, f, sub.ID, 1)
first := c.grant()
if first.Event.Snapshot.TaskID != task.ID || string(first.Event.Snapshot.Result) != string(result) {
t.Fatal("WSS snapshot changed large result")
}
c.reply(c.send("events.ping", struct{}{}, ""), "")
var leased int
if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM orbit_deliveries WHERE namespace_id=? AND subscription_id=? AND state='LEASED'`, f.token.NamespaceID, sub.ID).Scan(&leased); err != nil || leased != 1 {
t.Fatal("WSS capacity exceeded", err)
}
var ack events.AckResult
message := c.send("event.ack", eventProof(first), "")
if json.Unmarshal(c.reply(message, ""), &ack) != nil || ack.Deduplicated {
t.Fatal("first ack not applied")
}
second := c.grant()
if second.Event.ID == first.Event.ID {
t.Fatal("confirmed result redelivered")
}
var repeated events.AckResult
if json.Unmarshal(c.reply(c.send("event.ack", eventProof(first), message), ""), &repeated) != nil || !repeated.Deduplicated || !repeated.AcknowledgedAt.Equal(ack.AcknowledgedAt) {
t.Fatal("duplicate ACK changed receipt")
}
c.reply(c.send("event.ack", eventProof(second), ""), "")
})
t.Run("cross_node_redelivery_fences_old_proof", func(t *testing.T) {
f := newEventFixture(t, ctx, db)
sub := f.create(events.CreateInput{MaxAttempts: 2})
f.cancelResult()
a := connectTestEvents(t, ctx, serverA, f, sub.ID, 1)
first := a.grant()
a.conn.CloseNow()
f.expireDelivery(first)
b := connectTestEvents(t, ctx, serverB, f, sub.ID, 1)
second := b.grant()
if second.Event.ID != first.Event.ID || second.Delivery.ID != first.Delivery.ID || second.Delivery.LeaseEpoch != first.Delivery.LeaseEpoch+1 {
t.Fatal("cross-node redelivery lost durable identity")
}
b.reply(b.send("event.ack", eventProof(first), ""), "DELIVERY_LEASE_LOST")
b.reply(b.send("event.ack", eventProof(second), ""), "")
if f.subscription(sub.ID).PendingCount != 0 {
t.Fatal("cross-node ack did not release window")
}
})
t.Run("discarded_ack_response_reconciles_on_new_connection", func(t *testing.T) {
f := newEventFixture(t, ctx, db)
sub := f.create(events.CreateInput{})
f.cancelResult()
a := connectTestEvents(t, ctx, serverA, f, sub.ID, 1)
g := a.grant()
message := a.send("event.ack", eventProof(g), "")
deadline := time.Now().Add(3 * time.Second)
var original events.Delivery
for time.Now().Before(deadline) {
original, err = f.events.Delivery(ctx, f.token, sub.ID, g.Delivery.ID)
if err != nil {
t.Fatal(err)
}
if original.State == platform.DeliveryAcked {
break
}
time.Sleep(5 * time.Millisecond)
}
if original.State != platform.DeliveryAcked {
t.Fatal("ACK did not commit")
}
a.conn.CloseNow()
b := connectTestEvents(t, ctx, serverB, f, sub.ID, 1)
var receipt events.AckResult
if json.Unmarshal(b.reply(b.send("event.ack", eventProof(g), message), ""), &receipt) != nil || !receipt.Deduplicated || original.AcknowledgedAt == nil || !receipt.AcknowledgedAt.Equal(*original.AcknowledgedAt) {
t.Fatal("lost ACK response could not be reconciled")
}
if f.count("orbit_audit_events", "resource_id=? AND action='delivery.ack'", g.Delivery.ID) != 1 {
t.Fatal("replayed ACK duplicated audit")
}
})
t.Run("revocation_and_session_spoofing_close_live_connection", func(t *testing.T) {
f := newEventFixture(t, ctx, db)
sub := f.create(events.CreateInput{})
c := connectTestEvents(t, ctx, serverA, f, sub.ID, 1)
f.exec(`UPDATE orbit_credentials SET revoked_at=UTC_TIMESTAMP(6) WHERE namespace_id=? AND id=?`, f.token.NamespaceID, f.token.CredentialID)
// 主动心跳或下一次领取均重验权限;这里主动心跳固定相关响应。
msg := c.send("events.ping", struct{}{}, "")
frame := c.read()
if frame.Type != "error" || frame.Error == nil || frame.Error.Code != "UNAUTHENTICATED" || (frame.ReplyTo != "" && frame.ReplyTo != msg) {
t.Fatal("revoked connection retained authority")
}
g := newEventFixture(t, ctx, db)
owned := g.create(events.CreateInput{})
d := connectTestEvents(t, ctx, serverA, g, owned.ID, 1)
raw := `{"protocol_version":"1","type":"events.ping","message_id":"` + string(executionID(t)) + `","session_id":"` + string(executionID(t)) + `","body":{}}`
if err := d.conn.Write(ctx, websocket.MessageText, []byte(raw)); err != nil {
t.Fatal(err)
}
readCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
_, _, err := d.conn.Read(readCtx)
if websocket.CloseStatus(err) != websocket.StatusPolicyViolation {
t.Fatal("foreign connection session accepted")
}
})
t.Run("shutdown_closes_socket_preserves_delivery_and_rejects_new_connection", func(t *testing.T) {
f := newEventFixture(t, ctx, db)
sub := f.create(events.CreateInput{})
f.cancelResult()
a := connectTestEvents(t, ctx, serverA, f, sub.ID, 1)
g := a.grant()
bounded, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
if err := hubA.Shutdown(bounded); err != nil {
t.Fatal(err)
}
v, err := f.events.Delivery(ctx, f.token, sub.ID, g.Delivery.ID)
if err != nil || v.State != platform.DeliveryLeased || v.LeaseEpoch != g.Delivery.LeaseEpoch {
t.Fatal("shutdown erased authoritative lease", err)
}
eventHTTP(t, ctx, serverA, "GET", "/v1/events/connect", f.resources.Token, "", nil, 426)
headers := http.Header{}
headers.Set("Authorization", "Bearer "+f.resources.Token)
conn, response, err := websocket.Dial(ctx, strings.Replace(serverA.URL, "https://", "wss://", 1)+"/v1/events/connect", &websocket.DialOptions{HTTPClient: serverA.Client(), HTTPHeader: headers, Subprotocols: []string{httpserver.EventSubprotocol}})
if conn != nil {
conn.CloseNow()
}
if err == nil || response == nil || response.StatusCode != 503 {
t.Fatal("closing hub accepted new connection")
}
b := connectTestEvents(t, ctx, serverB, f, sub.ID, 1)
b.reply(b.send("event.ack", eventProof(g), ""), "")
})
}

View File

@@ -0,0 +1,5 @@
-- 面向已物化交付历史的稳定创建时间分页;确认和重新发送不会改变排序键。
-- 不使用外键或级联删除;序号和事件归属继续由服务端按命名空间校验。
ALTER TABLE orbit_deliveries
ADD KEY ix_delivery_history(namespace_id,subscription_id,created_at,id),
ADD KEY ix_delivery_state_history(namespace_id,subscription_id,state,created_at,id);

View File

@@ -88,7 +88,7 @@ func TestIntegrationNoForeignKeysUpgrade(t *testing.T) {
}
indexes := func() []string {
t.Helper()
rows, err := db.QueryContext(ctx, `SELECT CONCAT_WS(':',TABLE_NAME,INDEX_NAME,NON_UNIQUE,SEQ_IN_INDEX,COLUMN_NAME,COALESCE(SUB_PART,0),INDEX_TYPE,IS_VISIBLE) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME NOT IN ('orbit_application_resources','orbit_event_streams','orbit_result_events') AND INDEX_NAME NOT IN ('ix_task_priority_dispatch','ix_delivery_subscription_due','ix_delivery_subscription_lease') ORDER BY TABLE_NAME,INDEX_NAME,SEQ_IN_INDEX`)
rows, err := db.QueryContext(ctx, `SELECT CONCAT_WS(':',TABLE_NAME,INDEX_NAME,NON_UNIQUE,SEQ_IN_INDEX,COLUMN_NAME,COALESCE(SUB_PART,0),INDEX_TYPE,IS_VISIBLE) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME NOT IN ('orbit_application_resources','orbit_event_streams','orbit_result_events') AND INDEX_NAME NOT IN ('ix_task_priority_dispatch','ix_delivery_subscription_due','ix_delivery_subscription_lease','ix_delivery_history','ix_delivery_state_history') ORDER BY TABLE_NAME,INDEX_NAME,SEQ_IN_INDEX`)
if err != nil {
t.Fatal(err)
}

View File

@@ -0,0 +1,61 @@
// Package testcontract 在回归中校验实际报文与公开 Schema,服务二进制不依赖此包。
package testcontract
import (
"strings"
"sync"
"testing"
"git.yuntuo.hk/k2safe/orbit/protocol/eventsv1"
"github.com/santhosh-tekuri/jsonschema/v6"
)
var eventOnce sync.Once
var eventSchemas map[string]*jsonschema.Schema
var eventError error
// EventSchema 编译并缓存两个方向的 Schema;开启 date-time 等格式断言。
func EventSchema(t testing.TB, direction string) *jsonschema.Schema {
t.Helper()
eventOnce.Do(func() {
compiler := jsonschema.NewCompiler()
compiler.AssertFormat()
doc, err := jsonschema.UnmarshalJSON(strings.NewReader(eventsv1.Schema))
if err != nil {
eventError = err
return
}
const resource = "https://git.yuntuo.hk/k2safe/orbit/schema/events.v1.json"
if err := compiler.AddResource(resource, doc); err != nil {
eventError = err
return
}
eventSchemas = make(map[string]*jsonschema.Schema)
for _, side := range []string{"client", "server"} {
eventSchemas[side], eventError = compiler.Compile(resource + "#/$defs/" + side + "Frame")
if eventError != nil {
return
}
}
})
if eventError != nil {
t.Fatal(eventError)
}
schema := eventSchemas[direction]
if schema == nil {
t.Fatal("unknown Event protocol direction")
}
return schema
}
// EventFrame 验证真实传输的 JSON,不在错误中输出可能包含秘密的报文正文。
func EventFrame(t testing.TB, direction string, raw []byte) {
t.Helper()
value, err := jsonschema.UnmarshalJSON(strings.NewReader(string(raw)))
if err != nil {
t.Fatal("Event frame is not valid JSON")
}
if EventSchema(t, direction).Validate(value) != nil {
t.Fatalf("%s Event frame violates published Schema", direction)
}
}

View File

@@ -0,0 +1,175 @@
package httpserver
import (
"bytes"
"encoding/json"
"io"
"mime"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
"git.yuntuo.hk/k2safe/orbit/internal/jsonvalue"
core "git.yuntuo.hk/k2safe/orbit/model"
)
const EventSubprotocol = "orbit.events.v1"
const eventMaxFrame = 512 << 10
type eventFrame struct {
Version string `json:"protocol_version"`
Type string `json:"type"`
MessageID core.ID `json:"message_id"`
ReplyTo core.ID `json:"reply_to,omitempty"`
SessionID core.ID `json:"session_id,omitempty"`
Body json.RawMessage `json:"body,omitempty"`
Error *fault.Error `json:"error,omitempty"`
}
type eventHello struct {
SubscriptionID core.ID `json:"subscription_id"`
Capacity uint32 `json:"capacity"`
}
type eventWelcome struct {
SessionID core.ID `json:"session_id"`
SubscriptionID core.ID `json:"subscription_id"`
Capacity uint32 `json:"capacity"`
LeaseDurationMS int64 `json:"lease_duration_ms"`
HeartbeatIntervalMS int64 `json:"heartbeat_interval_ms"`
ServerTime time.Time `json:"server_time"`
Enabled bool `json:"enabled"`
}
type eventPong struct {
ServerTime time.Time `json:"server_time"`
Enabled bool `json:"enabled"`
}
func decodeEventBody(r *http.Request, out any) error {
content, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || content != "application/json" {
return fault.New("UNSUPPORTED_MEDIA_TYPE", "Content-Type must be application/json")
}
raw, err := io.ReadAll(io.LimitReader(r.Body, eventMaxFrame+1))
if err != nil {
return fault.Invalid("cannot read request body")
}
if len(raw) > eventMaxFrame {
return fault.New("BODY_TOO_LARGE", "request body exceeds 512 KiB")
}
return decodeEventJSON(raw, out)
}
func decodeEventJSON(raw []byte, out any) error {
if len(raw) == 0 || len(raw) > eventMaxFrame {
return fault.Invalid("invalid event message size")
}
if _, err := jsonvalue.Canonical(raw); err != nil {
return fault.Invalid("invalid event JSON")
}
typ := reflect.TypeOf(out).Elem()
if exactFields(raw, typ) != nil || requiredEventFields(raw, typ) != nil {
return fault.Invalid("missing, unknown or invalid event field")
}
d := json.NewDecoder(bytes.NewReader(raw))
d.DisallowUnknownFields()
if d.Decode(out) != nil {
return fault.Invalid("invalid event field value")
}
return nil
}
// requiredEventFields 同时验证显式控制意图及规范十进制字符串;可选历史游标允许0和null。
func requiredEventFields(raw []byte, typ reflect.Type) error {
if typ == reflect.TypeOf(json.RawMessage{}) || typ == reflect.TypeOf(time.Time{}) {
return nil
}
if typ.Kind() == reflect.Pointer {
if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return nil
}
return requiredEventFields(raw, typ.Elem())
}
if typ.Kind() == reflect.Array || typ.Kind() == reflect.Slice {
var values []json.RawMessage
if json.Unmarshal(raw, &values) != nil {
return fault.Invalid("invalid event array")
}
for _, v := range values {
if err := requiredEventFields(v, typ.Elem()); err != nil {
return err
}
}
return nil
}
if typ.Kind() != reflect.Struct {
return nil
}
var fields map[string]json.RawMessage
if json.Unmarshal(raw, &fields) != nil {
return fault.Invalid("invalid event object")
}
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
tags := strings.Split(field.Tag.Get("json"), ",")
if tags[0] == "" || tags[0] == "-" {
continue
}
optional, numberString := false, false
for _, v := range tags[1:] {
optional = optional || v == "omitempty"
numberString = numberString || v == "string"
}
value, found := fields[tags[0]]
if !found {
if optional {
continue
}
return fault.Invalid("missing event field")
}
ft := field.Type
nullable := ft.Kind() == reflect.Pointer
if nullable {
if bytes.Equal(bytes.TrimSpace(value), []byte("null")) {
continue
}
ft = ft.Elem()
}
if ft.Kind() == reflect.Uint64 && numberString {
var text string
if json.Unmarshal(value, &text) != nil {
return fault.Invalid("event counter must be a decimal string")
}
n, err := strconv.ParseUint(text, 10, 64)
if err != nil || strconv.FormatUint(n, 10) != text || (!nullable && n == 0) {
return fault.Invalid("invalid event counter")
}
}
if err := requiredEventFields(value, field.Type); err != nil {
return err
}
}
return nil
}
func decodeEventRequest(raw []byte, hello bool) (eventFrame, error) {
var frame eventFrame
if err := decodeEventJSON(raw, &frame); err != nil {
return frame, err
}
if frame.Version != "1" || frame.MessageID.Validate() != nil || len(frame.Body) == 0 {
return frame, fault.Invalid("invalid event envelope")
}
var fields map[string]json.RawMessage
_ = json.Unmarshal(raw, &fields)
if fields["error"] != nil || fields["reply_to"] != nil {
return frame, fault.Invalid("response fields are not allowed in event requests")
}
if hello {
if frame.Type != "events.hello" || fields["session_id"] != nil {
return frame, fault.Invalid("invalid event hello")
}
} else if frame.SessionID.Validate() != nil || (frame.Type != "event.ack" && frame.Type != "events.ping") {
return frame, fault.Invalid("invalid event session or type")
}
return frame, nil
}

View File

@@ -0,0 +1,141 @@
package httpserver
import (
"encoding/json"
"os"
"strings"
"testing"
"git.yuntuo.hk/k2safe/orbit/internal/testcontract"
"git.yuntuo.hk/k2safe/orbit/platform/events"
)
func TestEventFrameContract(t *testing.T) {
id := strings.Repeat("1", 32)
secret, _, err := events.NewSecret()
if err != nil {
t.Fatal(err)
}
valid := []string{
`{"protocol_version":"1","type":"events.hello","message_id":"` + id + `","body":{"subscription_id":"` + id + `","capacity":1}}`,
`{"protocol_version":"1","type":"events.ping","message_id":"` + id + `","session_id":"` + id + `","body":{}}`,
`{"protocol_version":"1","type":"event.ack","message_id":"` + id + `","session_id":"` + id + `","body":{"delivery_id":"` + id + `","epoch":"1","lease_secret":"` + secret + `"}}`,
}
for i, raw := range valid {
testcontract.EventFrame(t, "client", []byte(raw))
f, err := decodeEventRequest([]byte(raw), i == 0)
if err != nil {
t.Fatal(err)
}
switch i {
case 0:
var v eventHello
err = decodeEventJSON(f.Body, &v)
case 1:
var v struct{}
err = decodeEventJSON(f.Body, &v)
case 2:
var v events.AckInput
err = decodeEventJSON(f.Body, &v)
}
if err != nil {
t.Fatal(err)
}
}
for _, number := range []string{"0", "01", "18446744073709551616", "18446744073709551615"} {
raw := strings.Replace(valid[2], `"epoch":"1"`, `"epoch":"`+number+`"`, 1)
var frame eventFrame
_ = json.Unmarshal([]byte(raw), &frame)
var input events.AckInput
decodeErr := decodeEventJSON(frame.Body, &input)
var value any
_ = json.Unmarshal([]byte(raw), &value)
schemaErr := testcontract.EventSchema(t, "client").Validate(value)
wantValid := number == "18446744073709551615"
if (decodeErr == nil) != wantValid || (schemaErr == nil) != wantValid {
t.Fatal("event counter range disagrees with contract")
}
}
bad := []string{
strings.Replace(valid[0], `"body":`, `"session_id":null,"body":`, 1),
strings.Replace(valid[1], `"body":`, `"reply_to":"","body":`, 1),
strings.Replace(valid[1], `"body":`, `"error":null,"body":`, 1),
strings.Replace(valid[1], `"type":"events.ping"`, `"Type":"events.ping"`, 1),
strings.Replace(valid[1], `"protocol_version":"1"`, `"protocol_version":"1","protocol_version":"1"`, 1),
strings.Replace(valid[1], `"events.ping"`, `"worker.capacity"`, 1),
strings.Replace(valid[1], `"body":{}`, `"body":null`, 1),
}
for _, raw := range bad {
if _, err := decodeEventRequest([]byte(raw), strings.Contains(raw, "events.hello")); err == nil {
t.Fatal("accepted invalid event envelope")
}
}
// 标准Schema无法识别重复JSON键;实际解码器负责拒绝,其他方向约束由两者共同验证。
for _, raw := range bad[:4] {
var value any
if err := json.Unmarshal([]byte(raw), &value); err != nil {
t.Fatal(err)
}
if testcontract.EventSchema(t, "client").Validate(value) == nil {
t.Fatal("schema accepted direction or field violation")
}
}
}
func TestEventHTTPRequiredFieldsAndCounterCanonicalization(t *testing.T) {
id := strings.Repeat("1", 32)
for _, number := range []string{`"00"`, `"+1"`, `"01"`, `1`, `true`, `"18446744073709551616"`} {
var in events.CreateInput
raw := `{"name":"results","queue_id":"` + id + `","from_sequence":` + number + `}`
if decodeEventJSON([]byte(raw), &in) == nil {
t.Fatal("accepted invalid sequence")
}
}
for _, number := range []string{`"0"`, `null`, `"18446744073709551615"`} {
var in events.CreateInput
if err := decodeEventJSON([]byte(`{"name":"results","queue_id":"`+id+`","from_sequence":`+number+`}`), &in); err != nil {
t.Fatal(err)
}
}
for _, raw := range []string{`{"expected_revision":"1","reason":"pause"}`, `{"expected_revision":"01","reason":"pause","enabled":false}`, `{"expected_revision":"1","reason":"pause","enabled":null}`, `{"expected_revision":"1","reason":"pause","Enabled":false}`} {
var in events.ControlInput
if decodeEventJSON([]byte(raw), &in) == nil {
t.Fatal("accepted ambiguous control intent")
}
}
var good events.ControlInput
if err := decodeEventJSON([]byte(`{"expected_revision":"1","reason":"pause","enabled":false}`), &good); err != nil {
t.Fatal(err)
}
}
func TestEventDocumentationFramesMatchPublishedSchema(t *testing.T) {
raw, err := os.ReadFile("../../../docs/api/subscriptions.md")
if err != nil {
t.Fatal(err)
}
frames := 0
for _, block := range strings.Split(string(raw), "```json\n")[1:] {
snippet := strings.SplitN(block, "\n```", 2)[0]
var value map[string]json.RawMessage
if json.Unmarshal([]byte(snippet), &value) != nil {
t.Fatal("invalid documented JSON")
}
if value["protocol_version"] == nil {
continue
}
var kind string
if json.Unmarshal(value["type"], &kind) != nil {
t.Fatal("documented frame lacks type")
}
side := "server"
if kind == "events.hello" || kind == "events.ping" || kind == "event.ack" {
side = "client"
}
testcontract.EventFrame(t, side, []byte(snippet))
frames++
}
if frames != 6 {
t.Fatalf("expected six complete documented frames, got %d", frames)
}
}

View File

@@ -0,0 +1,356 @@
package httpserver
import (
"context"
"errors"
"net/http"
"strings"
"sync"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/dispatch"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
"git.yuntuo.hk/k2safe/orbit/internal/jsonwire"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/platform/events"
"github.com/coder/websocket"
)
// EventHub 仅负责已持久化结果的传输;断线不删除租约,消费者按事件ID持久化去重。
// 节点最多128条连接,领取池全局4个、每应用1个在途,队列加在途共128个。
type EventHub struct {
service *events.Service
ctx context.Context
cancel context.CancelFunc
mu sync.Mutex
closing bool
slots chan struct{}
wg sync.WaitGroup
dispatch *dispatch.Pool
poll time.Duration
}
func NewEventHub(parent context.Context, service *events.Service) *EventHub {
ctx, cancel := context.WithCancel(parent)
h := &EventHub{service: service, ctx: ctx, cancel: cancel, slots: make(chan struct{}, 128), poll: time.Second}
h.dispatch = dispatch.New(ctx, 4, 128)
h.wg.Add(1)
go func() { defer h.wg.Done(); <-h.dispatch.Done() }()
return h
}
func RegisterEventConsumers(server *http.Server, hub *EventHub) {
server.Handler.(*http.ServeMux).Handle("GET /v1/events/connect", hub)
}
func (h *EventHub) Shutdown(ctx context.Context) error {
h.mu.Lock()
h.closing = true
h.cancel()
h.mu.Unlock()
done := make(chan struct{})
go func() { h.wg.Wait(); close(done) }()
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (h *EventHub) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestID, err := core.NewID()
if err != nil {
writeError(w, "", fault.Unavailable)
return
}
w.Header().Set("X-Request-ID", string(requestID))
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Content-Type-Options", "nosniff")
if err = noEventQuery(r); err != nil {
writeError(w, string(requestID), err)
return
}
values := r.Header.Values("Authorization")
if len(values) != 1 {
writeError(w, string(requestID), fault.Unauthorized)
return
}
scheme, value, ok := strings.Cut(values[0], " ")
if !ok || !strings.EqualFold(scheme, "Bearer") {
writeError(w, string(requestID), fault.Unauthorized)
return
}
token, err := auth.Parse(value)
if err != nil {
writeError(w, string(requestID), err)
return
}
supported := false
for _, header := range r.Header.Values("Sec-WebSocket-Protocol") {
for _, p := range strings.Split(header, ",") {
supported = supported || strings.TrimSpace(p) == EventSubprotocol
}
}
if !supported {
writeJSON(w, http.StatusUpgradeRequired, map[string]any{"request_id": requestID, "error": fault.New("PROTOCOL_REQUIRED", "orbit.events.v1 subprotocol is required")})
return
}
h.mu.Lock()
if h.closing || h.ctx.Err() != nil {
h.mu.Unlock()
writeError(w, string(requestID), fault.Unavailable)
return
}
select {
case h.slots <- struct{}{}:
h.wg.Add(1)
default:
h.mu.Unlock()
w.Header().Set("Retry-After", "1")
writeJSON(w, 429, map[string]any{"request_id": requestID, "error": fault.New("CAPACITY_EXCEEDED", "node event connection capacity exceeded")})
return
}
h.mu.Unlock()
defer func() { <-h.slots; h.wg.Done() }()
authorize, stop := context.WithTimeout(r.Context(), 5*time.Second)
err = h.service.Authorize(authorize, token)
stop()
if err != nil {
writeError(w, string(requestID), err)
return
}
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{Subprotocols: []string{EventSubprotocol}, CompressionMode: websocket.CompressionDisabled})
if err != nil {
return
}
defer conn.CloseNow()
conn.SetReadLimit(eventMaxFrame)
ctx, cancel := context.WithCancel(h.ctx)
defer cancel()
helloCtx, helloStop := context.WithTimeout(ctx, 5*time.Second)
kind, raw, err := conn.Read(helloCtx)
helloStop()
if err != nil {
return
}
frame, decodeErr := decodeEventRequest(raw, true)
var input eventHello
if kind != websocket.MessageText || decodeErr != nil || decodeEventJSON(frame.Body, &input) != nil || input.SubscriptionID.Validate() != nil || input.Capacity < 1 || input.Capacity > 32 {
_ = conn.Close(websocket.StatusPolicyViolation, "invalid event hello")
return
}
call, callStop := context.WithTimeout(ctx, 8*time.Second)
binding, err := h.service.Bind(call, token, input.SubscriptionID)
callStop()
if err != nil {
_ = writeEventFrame(ctx, conn, eventFrame{Version: "1", Type: "error", MessageID: requestID, ReplyTo: frame.MessageID, Error: eventFault(err)})
return
}
session, err := core.NewID()
if err != nil {
return
}
welcome := eventWelcome{SessionID: session, SubscriptionID: input.SubscriptionID, Capacity: input.Capacity, LeaseDurationMS: events.LeaseDuration.Milliseconds(), HeartbeatIntervalMS: 15000, ServerTime: binding.ServerTime, Enabled: binding.Enabled}
body, err := jsonwire.Marshal(welcome)
if err != nil {
return
}
if err = writeEventFrame(ctx, conn, eventFrame{Version: "1", Type: "events.welcome", MessageID: requestID, ReplyTo: frame.MessageID, SessionID: session, Body: body}); err != nil {
return
}
h.consume(ctx, cancel, conn, token, binding, session, input.Capacity)
}
type eventClaimResult struct {
grant *events.Grant
err error
}
func (h *EventHub) claim(ctx context.Context, t auth.Token, sub, connection core.ID, capacity uint32) (out eventClaimResult) {
defer func() {
if recover() != nil {
out = eventClaimResult{err: fault.Unavailable}
}
}()
call, stop := context.WithTimeout(ctx, 8*time.Second)
defer stop()
out.grant, out.err = h.service.Claim(call, t, sub, events.ClaimInput{ConnectionID: connection, Capacity: capacity})
return out
}
func (h *EventHub) consume(ctx context.Context, cancel context.CancelFunc, conn *websocket.Conn, t auth.Token, b events.ConsumerBinding, session core.ID, capacity uint32) {
incoming := make(chan eventFrame, 4)
outgoing := make(chan outboundFrame, 8)
var loops sync.WaitGroup
loops.Add(2)
go func() {
defer loops.Done()
defer cancel()
for {
readCtx, stop := context.WithTimeout(ctx, 60*time.Second)
kind, raw, err := conn.Read(readCtx)
stop()
if err != nil {
return
}
frame, err := decodeEventRequest(raw, false)
if kind != websocket.MessageText || err != nil || frame.SessionID != session {
_ = conn.Close(websocket.StatusPolicyViolation, "invalid event frame")
return
}
select {
case incoming <- frame:
case <-ctx.Done():
return
}
}
}()
go func() {
defer loops.Done()
defer cancel()
for {
select {
case <-ctx.Done():
return
case frame := <-outgoing:
writeCtx, stop := context.WithTimeout(ctx, 5*time.Second)
err := conn.Write(writeCtx, websocket.MessageText, frame.data)
stop()
if frame.done != nil {
frame.done <- err
}
if err != nil {
return
}
}
}
}()
defer func() { cancel(); _ = conn.CloseNow(); loops.Wait() }()
send := func(frame eventFrame, wait bool) bool {
frame.Version = "1"
frame.SessionID = session
if frame.MessageID == "" {
var err error
frame.MessageID, err = core.NewID()
if err != nil {
return false
}
}
raw, err := jsonwire.Marshal(frame)
if err != nil || len(raw) > eventMaxFrame {
return false
}
out := outboundFrame{data: raw}
if wait {
out.done = make(chan error, 1)
}
select {
case outgoing <- out:
default:
return false
}
if !wait {
return true
}
select {
case err := <-out.done:
return err == nil
case <-ctx.Done():
return false
}
}
results := make(chan eventClaimResult, 1)
pending, burst := false, 0
request := func() {
if pending || burst == 0 || len(outgoing) >= 4 {
return
}
pending = h.dispatch.Submit(dispatch.Job{Context: ctx, Application: string(b.ApplicationID), Queue: string(t.NamespaceID) + "/" + string(b.SubscriptionID), Run: func(job context.Context) { results <- h.claim(job, t, b.SubscriptionID, session, capacity) }})
if pending {
burst--
}
}
ticker := time.NewTicker(h.poll)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case result := <-results:
pending = false
if result.err != nil {
send(eventFrame{Type: "error", Error: eventFault(result.err)}, true)
return
}
if result.grant == nil {
burst = 0
continue
}
raw, err := jsonwire.Marshal(result.grant)
if err != nil || !send(eventFrame{Type: "event.deliver", Body: raw}, false) {
return
}
request()
case frame := <-incoming:
call, stop := context.WithTimeout(ctx, 8*time.Second)
var body any
var err error
switch frame.Type {
case "event.ack":
var in events.AckInput
if err = decodeEventJSON(frame.Body, &in); err == nil {
body, err = h.service.Ack(call, t, b.SubscriptionID, in)
}
case "events.ping":
var in struct{}
if err = decodeEventJSON(frame.Body, &in); err == nil {
var current events.ConsumerBinding
current, err = h.service.Bind(call, t, b.SubscriptionID)
if err == nil {
body = eventPong{ServerTime: current.ServerTime, Enabled: current.Enabled}
}
}
default:
err = fault.Invalid("unsupported event message type")
}
stop()
reply := eventFrame{Type: "ack", ReplyTo: frame.MessageID}
if err != nil {
reply.Type = "error"
reply.Error = eventFault(err)
} else {
var marshalErr error
reply.Body, marshalErr = jsonwire.Marshal(body)
if marshalErr != nil {
return
}
}
fatal := err != nil && (fault.Code(err) == "UNAUTHENTICATED" || fault.Code(err) == "PERMISSION_DENIED" || fault.Code(err) == "NOT_FOUND" || fault.Code(err) == "UNAVAILABLE")
if !send(reply, fatal) || fatal {
return
}
case <-ticker.C:
if !pending {
burst = 4
request()
}
}
}
}
func eventFault(err error) *fault.Error {
var known *fault.Error
if errors.As(err, &known) {
return known
}
return fault.Unavailable
}
func writeEventFrame(ctx context.Context, conn *websocket.Conn, frame eventFrame) error {
raw, err := jsonwire.Marshal(frame)
if err != nil {
return err
}
if len(raw) > eventMaxFrame {
return fault.Invalid("event frame too large")
}
bounded, stop := context.WithTimeout(ctx, 5*time.Second)
defer stop()
return conn.Write(bounded, websocket.MessageText, raw)
}

View File

@@ -0,0 +1,139 @@
package httpserver
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/jsonwire"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/platform/events"
"github.com/coder/websocket"
)
type slowEventStore struct {
events.Store
once sync.Once
started chan struct{}
finished chan struct{}
}
func (s *slowEventStore) AuthorizeEvents(context.Context, auth.Token) error { return nil }
func (s *slowEventStore) BindConsumer(_ context.Context, _ auth.Token, id core.ID) (events.ConsumerBinding, error) {
return events.ConsumerBinding{ApplicationID: core.ID(strings.Repeat("1", 32)), SubscriptionID: id, QueueID: core.ID(strings.Repeat("2", 32)), Enabled: true, ServerTime: time.Now().UTC()}, nil
}
func (s *slowEventStore) ClaimDelivery(ctx context.Context, _ auth.Token, _ core.ID, _ events.ClaimInput) (*events.Grant, error) {
s.once.Do(func() { close(s.started) })
<-ctx.Done()
select {
case s.finished <- struct{}{}:
default:
}
return nil, ctx.Err()
}
func TestEventHubSlowClaimKeepsControlLiveAndBoundsConnections(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
store := &slowEventStore{started: make(chan struct{}), finished: make(chan struct{}, 4)}
hub := NewEventHub(ctx, events.New(store))
if cap(hub.slots) != 128 {
t.Fatal("unexpected production connection bound")
}
hub.slots = make(chan struct{}, 2)
hub.poll = 10 * time.Millisecond
server := New("", nil)
RegisterEventConsumers(server, hub)
tls := httptest.NewTLSServer(server.Handler)
defer tls.Close()
defer hub.Shutdown(context.Background())
id := core.ID(strings.Repeat("3", 32))
credential := core.ID(strings.Repeat("4", 32))
token, _, err := auth.Issue(id, credential)
if err != nil {
t.Fatal(err)
}
headers := http.Header{}
headers.Set("Authorization", "Bearer "+token)
dial := func() (*websocket.Conn, *http.Response, error) {
return websocket.Dial(ctx, strings.Replace(tls.URL, "https://", "wss://", 1)+"/v1/events/connect", &websocket.DialOptions{HTTPClient: tls.Client(), HTTPHeader: headers, Subprotocols: []string{EventSubprotocol}})
}
connect := func() (*websocket.Conn, core.ID) {
t.Helper()
conn, _, err := dial()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { conn.CloseNow() })
hello, _ := jsonwire.Marshal(eventHello{SubscriptionID: id, Capacity: 1})
raw, _ := jsonwire.Marshal(eventFrame{Version: "1", Type: "events.hello", MessageID: id, Body: hello})
if err = conn.Write(ctx, websocket.MessageText, raw); err != nil {
t.Fatal(err)
}
_, reply, err := conn.Read(ctx)
if err != nil {
t.Fatal(err)
}
var frame eventFrame
if json.Unmarshal(reply, &frame) != nil || frame.Type != "events.welcome" {
t.Fatal("missing welcome")
}
return conn, frame.SessionID
}
first, session := connect()
second, _ := connect()
select {
case <-store.started:
case <-ctx.Done():
t.Fatal("claim was not dispatched")
}
blocked, resp, err := dial()
if blocked != nil {
blocked.CloseNow()
}
if err == nil || resp == nil || resp.StatusCode != 429 || resp.Header.Get("Retry-After") != "1" {
t.Fatal("event connection bound not enforced")
}
ping, _ := jsonwire.Marshal(eventFrame{Version: "1", Type: "events.ping", MessageID: credential, SessionID: session, Body: json.RawMessage(`{}`)})
if err = first.Write(ctx, websocket.MessageText, ping); err != nil {
t.Fatal(err)
}
bounded, stop := context.WithTimeout(ctx, 500*time.Millisecond)
_, raw, err := first.Read(bounded)
stop()
if err != nil {
t.Fatal("slow claim blocked control reply")
}
var reply eventFrame
if json.Unmarshal(raw, &reply) != nil || reply.Type != "ack" || reply.ReplyTo != credential {
t.Fatal("missing ping acknowledgement")
}
second.CloseNow()
deadline := time.Now().Add(time.Second)
for len(hub.slots) == 2 && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if len(hub.slots) != 1 {
t.Fatal("disconnected consumer leaked connection slot")
}
third, _ := connect()
defer third.CloseNow()
bounded, stop = context.WithTimeout(ctx, time.Second)
defer stop()
if err = hub.Shutdown(bounded); err != nil {
t.Fatal("shutdown leaked claim or connection", err)
}
select {
case <-store.finished:
default:
t.Fatal("shutdown did not cancel in-flight claim")
}
if len(hub.slots) != 0 {
t.Fatal("shutdown leaked connection slots")
}
}

View File

@@ -0,0 +1,128 @@
package httpserver
import (
"net/http"
"net/url"
"strconv"
"strings"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/platform/events"
platform "git.yuntuo.hk/k2safe/orbit/platform/model"
)
func RegisterEventSubscriptions(server *http.Server, s *events.Service) {
mux := server.Handler.(*http.ServeMux)
mux.HandleFunc("POST /v1/subscriptions", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
if err := noEventQuery(r); err != nil {
return nil, 0, err
}
var in events.CreateInput
if err := decodeEventBody(r, &in); err != nil {
return nil, 0, err
}
key, err := idempotencyKey(r)
if err != nil {
return nil, 0, err
}
out, err := s.Create(r.Context(), t, key, in)
return out, createdStatus(out.Deduplicated), err
}))
mux.HandleFunc("GET /v1/subscriptions", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
queue, in, err := eventQuery(r, true)
if err != nil {
return nil, 0, err
}
out, err := s.List(r.Context(), t, queue, in)
return out, 200, err
}))
mux.HandleFunc("GET /v1/subscriptions/{id}", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
if err := noEventQuery(r); err != nil {
return nil, 0, err
}
out, err := s.Get(r.Context(), t, core.ID(r.PathValue("id")))
return out, 200, err
}))
mux.HandleFunc("POST /v1/subscriptions/{command}", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
id, command, ok := strings.Cut(r.PathValue("command"), ":")
if !ok || command != "configure" {
return nil, 0, fault.NotFound
}
if err := noEventQuery(r); err != nil {
return nil, 0, err
}
var in events.ControlInput
if err := decodeEventBody(r, &in); err != nil {
return nil, 0, err
}
key, err := idempotencyKey(r)
if err != nil {
return nil, 0, err
}
out, err := s.Control(r.Context(), t, core.ID(id), key, in)
return out, 200, err
}))
mux.HandleFunc("GET /v1/subscriptions/{id}/deliveries", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
_, in, err := eventQuery(r, false)
if err != nil {
return nil, 0, err
}
out, err := s.Deliveries(r.Context(), t, core.ID(r.PathValue("id")), in)
return out, 200, err
}))
mux.HandleFunc("GET /v1/subscriptions/{id}/deliveries/{delivery}", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
if err := noEventQuery(r); err != nil {
return nil, 0, err
}
out, err := s.Detail(r.Context(), t, core.ID(r.PathValue("id")), core.ID(r.PathValue("delivery")))
return out, 200, err
}))
mux.HandleFunc("POST /v1/subscriptions/{id}/deliveries/{command}", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
delivery, command, ok := strings.Cut(r.PathValue("command"), ":")
if !ok || command != "retry" {
return nil, 0, fault.NotFound
}
if err := noEventQuery(r); err != nil {
return nil, 0, err
}
var in events.RetryInput
if err := decodeEventBody(r, &in); err != nil {
return nil, 0, err
}
key, err := idempotencyKey(r)
if err != nil {
return nil, 0, err
}
out, err := s.Retry(r.Context(), t, core.ID(r.PathValue("id")), core.ID(delivery), key, in)
return out, 200, err
}))
}
func noEventQuery(r *http.Request) error {
if r.URL.RawQuery != "" || r.URL.ForceQuery {
return fault.Invalid("query parameters are not allowed")
}
return nil
}
func eventQuery(r *http.Request, subscriptions bool) (core.ID, events.PageInput, error) {
var in events.PageInput
q, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return "", in, fault.Invalid("invalid query encoding")
}
for k, v := range q {
if len(v) != 1 || (k != "limit" && k != "cursor" && !(subscriptions && k == "queue_id") && !(!subscriptions && k == "state")) {
return "", in, fault.Invalid("invalid subscription query")
}
}
if q.Has("limit") {
in.Limit, err = strconv.Atoi(q.Get("limit"))
if err != nil || in.Limit < 1 || strconv.Itoa(in.Limit) != q.Get("limit") {
return "", in, fault.Invalid("invalid limit")
}
}
in.Cursor = q.Get("cursor")
in.State = platform.DeliveryState(q.Get("state"))
return core.ID(q.Get("queue_id")), in, nil
}

View File

@@ -185,7 +185,7 @@ func writeError(w http.ResponseWriter, id string, err error) {
if !errors.As(err, &known) {
known = fault.Unavailable
}
status := map[string]int{"INVALID_ARGUMENT": 400, "UNAUTHENTICATED": 401, "PERMISSION_DENIED": 403, "NOT_FOUND": 404, "STATE_CONFLICT": 409, "IDEMPOTENCY_CONFLICT": 409, "BODY_TOO_LARGE": 413, "UNSUPPORTED_MEDIA_TYPE": 415}[known.Code]
status := map[string]int{"INVALID_ARGUMENT": 400, "UNAUTHENTICATED": 401, "PERMISSION_DENIED": 403, "NOT_FOUND": 404, "STATE_CONFLICT": 409, "IDEMPOTENCY_CONFLICT": 409, "BODY_TOO_LARGE": 413, "UNSUPPORTED_MEDIA_TYPE": 415, "DELIVERY_LEASE_LOST": 409, "EVENT_HISTORY_EXPIRED": 410, "SUBSCRIPTION_LIMIT": 409}[known.Code]
if status == 0 {
status = 503
}