feat: tune websocket hub backpressure

This commit is contained in:
2026-09-20 20:53:50 +08:00
parent 850376624f
commit 478be069e6
16 changed files with 218 additions and 52 deletions

View File

@@ -31,10 +31,11 @@ type EventHub struct {
poll time.Duration
}
func NewEventHub(parent context.Context, service *events.Service) *EventHub {
func NewEventHub(parent context.Context, service *events.Service, options ...HubOptions) *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)
limits := resolveHubOptions(options)
h := &EventHub{service: service, ctx: ctx, cancel: cancel, slots: make(chan struct{}, limits.ConnectionLimit), poll: time.Second}
h.dispatch = dispatch.New(ctx, limits.DispatchWorkers, limits.DispatchCapacity)
h.wg.Add(1)
go func() { defer h.wg.Done(); <-h.dispatch.Done() }()
return h

View File

@@ -0,0 +1,48 @@
package httpserver
// HubOptions 是节点内 Worker/Event WebSocket Hub 的有界运行参数。
// 这些参数只控制本节点内存和领取并发,不改变 MySQL 中的授权、租约或配额事实。
type HubOptions struct {
// ConnectionLimit 是该 Hub 允许的同时连接数;超过后返回 429。
ConnectionLimit int
// DispatchWorkers 是本 Hub 同时执行数据库领取调用的 Go 协程上限。
DispatchWorkers int
// DispatchCapacity 是领取调用的有界内存排队槽位;满额时连接等待下一轮扫描。
DispatchCapacity int
}
const (
defaultHubConnectionLimit = 128
defaultHubDispatchWorkers = 4
defaultHubDispatchCapacity = 128
maxHubConnectionLimit = 4096
maxHubDispatchWorkers = 256
maxHubDispatchCapacity = 65536
)
func resolveHubOptions(options []HubOptions) HubOptions {
out := HubOptions{
ConnectionLimit: defaultHubConnectionLimit,
DispatchWorkers: defaultHubDispatchWorkers,
DispatchCapacity: defaultHubDispatchCapacity,
}
if len(options) > 1 {
panic("at most one HubOptions value is allowed")
}
if len(options) == 1 {
in := options[0]
if in.ConnectionLimit != 0 {
out.ConnectionLimit = in.ConnectionLimit
}
if in.DispatchWorkers != 0 {
out.DispatchWorkers = in.DispatchWorkers
}
if in.DispatchCapacity != 0 {
out.DispatchCapacity = in.DispatchCapacity
}
}
if out.ConnectionLimit < 1 || out.ConnectionLimit > maxHubConnectionLimit || out.DispatchWorkers < 1 || out.DispatchWorkers > maxHubDispatchWorkers || out.DispatchCapacity < 1 || out.DispatchCapacity > maxHubDispatchCapacity {
panic("HubOptions values exceed bounded range")
}
return out
}

View File

@@ -0,0 +1,42 @@
package httpserver
import (
"context"
"testing"
"git.yuntuo.hk/k2safe/orbit/platform/events"
"git.yuntuo.hk/k2safe/orbit/pulse/worker"
)
func TestHubOptionsAreBoundedAndApplied(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
workerOptions := resolveHubOptions([]HubOptions{{ConnectionLimit: 7, DispatchWorkers: 3, DispatchCapacity: 11}})
if workerOptions.ConnectionLimit != 7 || workerOptions.DispatchWorkers != 3 || workerOptions.DispatchCapacity != 11 {
t.Fatalf("worker hub options not resolved: %+v", workerOptions)
}
workerHub := NewWorkerHub(ctx, worker.New(nil), workerOptions)
if cap(workerHub.slots) != 7 {
t.Fatalf("worker connection limit not applied: %d", cap(workerHub.slots))
}
eventOptions := resolveHubOptions([]HubOptions{{ConnectionLimit: 5, DispatchWorkers: 2, DispatchCapacity: 9}})
eventHub := NewEventHub(ctx, events.New(nil), eventOptions)
if cap(eventHub.slots) != 5 {
t.Fatalf("event connection limit not applied: %d", cap(eventHub.slots))
}
workerHub.Shutdown(context.Background())
eventHub.Shutdown(context.Background())
}
func TestHubOptionsRejectInvalidValues(t *testing.T) {
for _, options := range []HubOptions{{ConnectionLimit: -1}, {ConnectionLimit: maxHubConnectionLimit + 1}, {DispatchWorkers: -1}, {DispatchWorkers: maxHubDispatchWorkers + 1}, {DispatchCapacity: -1}, {DispatchCapacity: maxHubDispatchCapacity + 1}} {
t.Run("invalid", func(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("invalid hub options did not panic")
}
}()
resolveHubOptions([]HubOptions{options})
})
}
}

View File

@@ -38,10 +38,11 @@ type WorkerHub struct {
emptyPolls map[[32]byte]time.Time
}
func NewWorkerHub(parent context.Context, service *worker.Service) *WorkerHub {
func NewWorkerHub(parent context.Context, service *worker.Service, options ...HubOptions) *WorkerHub {
ctx, cancel := context.WithCancel(parent)
h := &WorkerHub{service: service, ctx: ctx, cancel: cancel, slots: make(chan struct{}, 128), poll: time.Second, emptyPolls: make(map[[32]byte]time.Time)}
h.dispatch = dispatch.New(ctx, 4, 128)
limits := resolveHubOptions(options)
h := &WorkerHub{service: service, ctx: ctx, cancel: cancel, slots: make(chan struct{}, limits.ConnectionLimit), poll: time.Second, emptyPolls: make(map[[32]byte]time.Time)}
h.dispatch = dispatch.New(ctx, limits.DispatchWorkers, limits.DispatchCapacity)
h.wg.Add(1)
go func() { defer h.wg.Done(); <-h.dispatch.Done() }()
return h