fix: 完成 broker 复审 B-03 至 B-12

每连接异步下发与背压、写出后断开、校验当前连接与订阅、生命周期串行、登录条件更新、闲置按在线计、认证超时并发与 Shutdown 0x8B。
This commit is contained in:
Nixevol
2026-09-30 16:21:05 +08:00
parent 0b9ce0359a
commit 91e887ba46
12 changed files with 1099 additions and 89 deletions
+134 -43
View File
@@ -37,7 +37,20 @@ var ErrNoConnection = errors.New("broker: no active connection")
// ErrLargeFrameTimeout 全局大帧名额在有界等待内拿不到。
var ErrLargeFrameTimeout = errors.New("broker: large frame quota timeout")
const largeAcquireWait = 5 * time.Second
// ErrBackpressure 该连接下行队列已满(帧数或字节数)。
var ErrBackpressure = errors.New("broker: downlink backpressure")
// ErrNotSubscribed 当前连接尚未订阅下行主题。
var ErrNotSubscribed = errors.New("broker: down topic not subscribed")
// ErrSessionWriteConflict 密码登录写令牌时发现库已被并发更新。
var ErrSessionWriteConflict = errors.New("broker: session token write conflict")
const (
largeAcquireWait = 5 * time.Second
downQueueMax = 256
downQueueBytes = 16 << 20
)
// AuthResult 是登录校验结论(N3 实现真实逻辑;N2 默认拒绝)。
type AuthResult struct {
@@ -100,6 +113,9 @@ type Broker struct {
largeSem chan struct{}
closed atomic.Bool
lifeMu sync.Mutex
lifeLocks map[string]*sync.Mutex
}
type connState struct {
@@ -120,6 +136,13 @@ type connState struct {
metricsCounted bool
established bool
createdAt time.Time
closing bool
superseded bool
downCh chan downItem
downStop chan struct{}
downDone chan struct{}
downBytes atomic.Int64
sentPub atomic.Int64
mu sync.Mutex
handshakeTimer *time.Timer
@@ -162,18 +185,19 @@ func New(opts Options) (*Broker, error) {
})
b := &Broker{
server: srv,
auth: auth,
uplink: uplink,
log: log,
onDrop: opts.OnPublishDropped,
metrics: opts.Metrics,
current: make(map[string]*connState),
byClient: make(map[*mqtt.Client]*connState),
byConnID: make(map[port.ConnID]*connState),
closedCh: make(chan struct{}),
queues: make(map[string]*uplinkQueue),
largeSem: make(chan struct{}, largeFrameSlots),
server: srv,
auth: auth,
uplink: uplink,
log: log,
onDrop: opts.OnPublishDropped,
metrics: opts.Metrics,
current: make(map[string]*connState),
byClient: make(map[*mqtt.Client]*connState),
byConnID: make(map[port.ConnID]*connState),
closedCh: make(chan struct{}),
queues: make(map[string]*uplinkQueue),
largeSem: make(chan struct{}, largeFrameSlots),
lifeLocks: make(map[string]*sync.Mutex),
}
b.hook = &nixHook{b: b}
if err := srv.AddHook(b.hook, nil); err != nil {
@@ -203,10 +227,36 @@ func (b *Broker) Close() error {
for _, q := range b.queues {
q.close()
}
b.queues = make(map[string]*uplinkQueue)
b.queuesMu.Unlock()
return b.server.Close()
}
// Shutdown 向所有连接发 MQTT 5 0x8B 后关闭。完整 HTTP 停机顺序见 L-03。
func (b *Broker) Shutdown(ctx context.Context) error {
if b.closed.Load() {
return nil
}
b.connsMu.RLock()
clients := make([]*mqtt.Client, 0, len(b.byClient))
for cl := range b.byClient {
if cl != nil {
clients = append(clients, cl)
}
}
b.connsMu.RUnlock()
for _, cl := range clients {
_ = b.server.DisconnectClient(cl, packets.ErrServerShuttingDown)
}
if ctx != nil {
select {
case <-ctx.Done():
default:
}
}
return b.Close()
}
// AttachTCP 把裸 TCP/TLS 连接交给 mochi;阻塞到连接结束。
func (b *Broker) AttachTCP(conn net.Conn) error {
return b.server.EstablishConnection("tcp", conn)
@@ -219,44 +269,55 @@ func (b *Broker) AttachWS(conn net.Conn) error {
// PublishDown 实现 port.Downlink。
func (b *Broker) PublishDown(ctx context.Context, endpointID string, connID port.ConnID, payload []byte, opts port.PublishOpts) error {
if b.closed.Load() {
return errors.New("broker: closed")
}
st := b.lookupConn(endpointID, connID)
if st == nil {
return ErrNoConnection
}
limit := effectivePayloadLimit(st.maxPacketSize, st.maxRecvBytes)
if limit > 0 && len(payload) > limit {
return ErrPayloadTooLarge
}
qos := opts.QoS
if qos > 1 {
qos = 1
}
topic := downTopic(endpointID)
large := len(payload) > largeFrameBytes
if large {
if err := b.acquireLarge(ctx); err != nil {
return err
}
st.mu.Lock()
st.largePending++
st.mu.Unlock()
return b.enqueueDownlink(endpointID, connID, payload, qos, "")
}
// PublishThenDisconnect 把一帧写入该连接下行队列,写出后再断开(无固定 sleep)。
func (b *Broker) PublishThenDisconnect(_ context.Context, endpointID string, connID port.ConnID, payload []byte, qos byte, reason port.DisconnectReason) error {
if qos > 1 {
qos = 1
}
if reason == "" {
reason = port.DisconnectNormal
}
return b.enqueueDownlink(endpointID, connID, payload, qos, reason)
}
func (b *Broker) enqueueDownlink(endpointID string, connID port.ConnID, payload []byte, qos byte, disconnect port.DisconnectReason) error {
if b.closed.Load() {
return errors.New("broker: closed")
}
st := b.lookupCurrent(endpointID, connID)
if st == nil {
return ErrNoConnection
}
if err := b.server.Publish(topic, payload, false, qos); err != nil {
if large {
b.finishLargePublish(st)
}
return err
st.mu.Lock()
maxRecv := st.maxRecvBytes
closing := st.closing
superseded := st.superseded
st.mu.Unlock()
if closing || superseded {
return ErrNoConnection
}
if large {
b.finishLargePublish(st)
if !b.hasDownSub(st) {
return ErrNotSubscribed
}
return nil
limit := EffectivePayloadLimit(st.maxPacketSize, maxRecv)
if limit > 0 && len(payload) > limit {
return ErrPayloadTooLarge
}
return st.enqueueDown(downItem{
payload: append([]byte(nil), payload...),
qos: qos,
disconnect: disconnect,
})
}
func (b *Broker) acquireLarge(ctx context.Context) error {
@@ -367,6 +428,31 @@ func (b *Broker) lookupConn(endpointID string, connID port.ConnID) *connState {
return b.current[endpointID]
}
// lookupCurrent 只返回该端当前连接;connID 非空时必须仍是当前连接。
func (b *Broker) lookupCurrent(endpointID string, connID port.ConnID) *connState {
b.connsMu.RLock()
defer b.connsMu.RUnlock()
cur := b.current[endpointID]
if cur == nil {
return nil
}
if connID != "" && cur.connID != connID {
return nil
}
return cur
}
func (b *Broker) endpointLife(endpointID string) *sync.Mutex {
b.lifeMu.Lock()
defer b.lifeMu.Unlock()
m := b.lifeLocks[endpointID]
if m == nil {
m = &sync.Mutex{}
b.lifeLocks[endpointID] = m
}
return m
}
func downTopic(endpointID string) string {
return "nix/c/" + endpointID + "/down"
}
@@ -375,6 +461,11 @@ func upTopic(endpointID string) string {
return "nix/c/" + endpointID + "/up"
}
// EffectivePayloadLimit 下行载荷上限:客户端 Maximum Packet Size 减包头预留,再与 max_receive_bytes 取更严者。
func EffectivePayloadLimit(maxPacketSize uint32, maxRecvBytes int) int {
return effectivePayloadLimit(maxPacketSize, maxRecvBytes)
}
func effectivePayloadLimit(maxPacketSize uint32, maxRecvBytes int) int {
limit := 0
if maxPacketSize > 0 {