Files
creator-hub/internal/controlplane/api/event_channel_test.go
T
rogee e4c0186f96
douyin-release-gate / verify (push) Failing after 21m22s
fix(events): isolate subscription overflow instead of killing the shared channel
One slow account's full subscription buffer no longer tears down the
WebSocket shared by all accounts on the gateway. The overflowing
subscription is retired with an explicit error and replays unacked
deliveries on resubscribe; the buffer grows from 2 to 64 messages.
2026-10-08 21:57:12 +08:00

255 lines
7.6 KiB
Go

package api
import (
"context"
"errors"
"fmt"
"git.ipao.vip/rogee/creator-hub/internal/environment"
"github.com/gorilla/websocket"
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestEventChannelSharesConnectionAndAcknowledges(t *testing.T) {
var connections atomic.Int32
acks := make(chan struct{}, 2)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/channel" || r.Header.Get("Authorization") != "Bearer test" {
t.Errorf("unexpected channel request")
w.WriteHeader(400)
return
}
c, err := (&websocket.Upgrader{}).Upgrade(w, r, nil)
if err != nil {
t.Error(err)
return
}
defer c.Close()
connections.Add(1)
for {
var v map[string]any
if c.ReadJSON(&v) != nil {
return
}
switch v["type"] {
case "subscribe":
_ = c.WriteJSON(map[string]any{"type": "subscribed", "subscription": v["subscription"]})
_ = c.WriteJSON(map[string]any{"type": "deliveries", "subscription": v["subscription"], "deliveries": []any{map[string]any{"kind": "open", "delivery_id": v["alias"]}}})
case "ack":
acks <- struct{}{}
}
}
}))
defer server.Close()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
target := environment.Gateway{Endpoint: server.URL, Token: "test"}
first, err := acquireEventSubscription(ctx, target, map[string]any{"alias": "a"})
if err != nil {
t.Fatal(err)
}
defer first.close()
second, err := acquireEventSubscription(ctx, target, map[string]any{"alias": "b"})
if err != nil {
t.Fatal(err)
}
defer second.close()
if connections.Load() != 1 {
t.Fatalf("connections=%d", connections.Load())
}
items, err := first.poll(ctx, nil)
if err != nil || items[0].DeliveryID != "a" {
t.Fatalf("%+v %v", items, err)
}
items, err = second.poll(ctx, nil)
if err != nil || items[0].DeliveryID != "b" {
t.Fatalf("%+v %v", items, err)
}
short, c := context.WithTimeout(ctx, 30*time.Millisecond)
defer c()
_, _ = first.poll(short, []string{"a"})
select {
case <-acks:
case <-ctx.Done():
t.Fatal("ACK not received")
}
}
func TestEventChannelRejectsHandshakeAndSubscriptionTimeout(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(503) }))
defer server.Close()
for _, endpoint := range []string{server.URL, "invalid://gateway", "http://%"} {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
_, err := acquireEventSubscription(ctx, environment.Gateway{Endpoint: endpoint}, map[string]any{"alias": "a"})
cancel()
if err == nil {
t.Fatalf("invalid connection accepted: %s", endpoint)
}
}
waiting := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, e := (&websocket.Upgrader{}).Upgrade(w, r, nil)
if e != nil {
return
}
defer c.Close()
for {
var value map[string]any
if c.ReadJSON(&value) != nil {
return
}
}
}))
defer waiting.Close()
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Millisecond)
defer cancel()
_, err := acquireEventSubscription(ctx, environment.Gateway{Endpoint: waiting.URL}, map[string]any{"alias": "a"})
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("subscription wait did not honor cancellation: %v", err)
}
}
func TestEventChannelReportsDisconnect(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, e := (&websocket.Upgrader{}).Upgrade(w, r, nil)
if e != nil {
return
}
defer c.Close()
var v map[string]any
_ = c.ReadJSON(&v)
_ = c.WriteJSON(map[string]any{"type": "subscribed", "subscription": v["subscription"]})
}))
defer server.Close()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
sub, err := acquireEventSubscription(ctx, environment.Gateway{Endpoint: server.URL}, map[string]any{"alias": "a"})
if err == nil {
defer sub.close()
_, err = sub.poll(ctx, nil)
}
if err == nil {
t.Fatal("disconnect hidden")
}
}
func TestEventChannelSubscriptionOverflowIsolatesAccounts(t *testing.T) {
var connections atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := (&websocket.Upgrader{}).Upgrade(w, r, nil)
if err != nil {
t.Error(err)
return
}
defer c.Close()
connections.Add(1)
var write sync.Mutex
send := func(v map[string]any) {
write.Lock()
defer write.Unlock()
_ = c.WriteJSON(v)
}
for {
var v map[string]any
if c.ReadJSON(&v) != nil {
return
}
if v["type"] != "subscribe" {
continue
}
send(map[string]any{"type": "subscribed", "subscription": v["subscription"]})
if v["alias"] == "flooded" {
for i := 0; i < 80; i++ {
send(map[string]any{"type": "deliveries", "subscription": v["subscription"],
"deliveries": []any{map[string]any{"kind": "open", "delivery_id": fmt.Sprintf("flood-%d", i)}}})
}
}
if v["alias"] == "late" {
send(map[string]any{"type": "deliveries", "subscription": v["subscription"],
"deliveries": []any{map[string]any{"kind": "open", "delivery_id": "late-1"}}})
}
}
}))
defer server.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
target := environment.Gateway{Endpoint: server.URL, Token: "test"}
flooded, err := acquireEventSubscription(ctx, target, map[string]any{"alias": "flooded"})
if err != nil {
t.Fatal(err)
}
defer flooded.close()
late, err := acquireEventSubscription(ctx, target, map[string]any{"alias": "late"})
if err != nil {
t.Fatalf("shared channel refused a joining account after another account overflowed: %v", err)
}
defer late.close()
items, err := late.poll(ctx, nil)
if err != nil || items[0].DeliveryID != "late-1" {
t.Fatalf("other account deliveries broke after one account overflowed: %+v %v", items, err)
}
if connections.Load() != 1 {
t.Fatalf("overflow tore down the shared channel, connections=%d", connections.Load())
}
// Buffered deliveries sent before the overflow fired remain valid work; the
// subscription keeps reporting them until the buffer drains, then surfaces
// the explicit overflow error.
for {
_, err = flooded.poll(ctx, nil)
if err != nil {
if !strings.Contains(err.Error(), "overflow") {
t.Fatalf("overflowed subscription must fail with an explicit overflow error, got %v", err)
}
break
}
}
}
func TestEventChannelClosingLastAccountDoesNotRetireJoiningAccount(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := (&websocket.Upgrader{}).Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
var value map[string]any
if c.ReadJSON(&value) != nil {
return
}
if value["type"] == "subscribe" {
_ = c.WriteJSON(map[string]any{"type": "subscribed", "subscription": value["subscription"]})
}
}
}))
defer server.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
target := environment.Gateway{Endpoint: server.URL}
previous, err := acquireEventSubscription(ctx, target, map[string]any{"alias": "a"})
if err != nil {
t.Fatal(err)
}
defer func() { previous.close() }()
for i := 0; i < 50; i++ {
type result struct {
sub *eventSubscription
err error
}
joined := make(chan result, 1)
go func() {
sub, err := acquireEventSubscription(ctx, target, map[string]any{"alias": "b"})
joined <- result{sub, err}
}()
previous.close()
next := <-joined
if next.err != nil {
t.Fatalf("joining account interrupted at iteration %d: %v", i, next.err)
}
previous = next.sub
}
}