douyin-release-gate / verify (push) Failing after 21m22s
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.
255 lines
7.6 KiB
Go
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
|
|
}
|
|
}
|