You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义并发队列引发index out of range等Panic问题求助

匹配服务器Panic问题排查与修复

问题1:索引越界(index out of range)Panic

原因

handleFindPeer中启动的goroutine里,queue.Len() > 0和queue.Dequeue()是两个独立操作,没有原子性保障。当多个请求并发时,可能出现多个goroutine同时检测到队列非空,第一个goroutine执行Dequeue取走最后一个元素,后续goroutine再执行Dequeue时队列已空,触发索引越界。

修复方案

给ClientQueue新增TryDequeue方法,在锁内部完成「检查队列长度+取出元素」的原子操作:

// client.go 中新增方法
func (q *ClientQueue) TryDequeue() (Client, bool) {
    q.mu.Lock()
    defer q.mu.Unlock()
    if len(q.clients) == 0 {
        return Client{}, false
    }
    client := q.clients[0]
    q.clients = q.clients[1:]
    return client, true
}

修改handleFindPeer中的goroutine逻辑,用TryDequeue替代原有的判断+Dequeue:

go func() {
    peer, ok := queue.TryDequeue()
    if ok {
        sendPairingResponse(client, peer)
        close(client.peerMatchedCh)
        close(peer.peerMatchedCh)
        log.Printf("Paired %v and %v\n", client.addr, peer.addr)
    } else {
        queue.Enqueue(client)
        // 监听上下文取消,超时后从队列移除当前客户端
        select {
        case <-ctx.Done():
            queue.Remove(client)
            log.Printf("Removed timed out client %v from queue\n", client.addr)
        case <-client.peerMatchedCh:
            // 配对成功,无需移除
        }
    }
}()

问题2:WriteHeader called after Handler finished Panic

原因

当客户端等待配对超时后,handleFindPeer中的select会进入ctx.Done()分支,调用http.Error写入响应,此时Handler已结束。但如果该客户端仍在队列中,后续被取出配对时,sendPairingResponse会再次尝试写入响应,导致操作已关闭的响应writer,触发panic。同时,超时客户端未被从队列移除,会导致无效客户端残留。

修复方案

  1. 给Client结构体添加sync.Once,确保每个响应只被写入一次:
// client.go 修改Client结构体
type Client struct {
    addr          string
    respWriter    http.ResponseWriter
    peerMatchedCh chan struct{}
    writeOnce     sync.Once // 新增,保护响应写入操作
}
  1. 修改sendPairingResponse,用writeOnce确保响应只写入一次:
// main.go 修改sendPairingResponse
func sendPairingResponse(c1, c2 Client) {
    c1.writeOnce.Do(func() {
        c1.respWriter.WriteHeader(http.StatusOK)
        _, _ = c1.respWriter.Write([]byte(c2.addr))
        // 强制刷新响应,避免客户端等待
        if flusher, ok := c1.respWriter.(http.Flusher); ok {
            flusher.Flush()
        }
    })
    c2.writeOnce.Do(func() {
        c2.respWriter.WriteHeader(http.StatusOK)
        _, _ = c2.respWriter.Write([]byte(c1.addr))
        if flusher, ok := c2.respWriter.(http.Flusher); ok {
            flusher.Flush()
        }
    })
}
  1. 修改超时分支的响应写入,同样用writeOnce保护:
// main.go 修改handleFindPeer的select分支
select {
case <-client.peerMatchedCh:
    return
case <-ctx.Done():
    client.writeOnce.Do(func() {
        http.Error(w, "timeout waiting for peer", http.StatusRequestTimeout)
    })
}

额外优化点

  • ClientQueue.Remove方法中,用地址匹配替代addr匹配更可靠(不同客户端可能出现同一addr的情况,比如短连接复用端口),可以给Client改为指针存储:
// 修改ClientQueue为存储指针
type ClientQueue struct {
    clients []*Client
    mu      sync.Mutex
}

// 对应修改Enqueue、TryDequeue、Remove等方法
func (q *ClientQueue) Enqueue(c *Client) {
    q.mu.Lock()
    defer q.mu.Unlock()
    q.clients = append(q.clients, c)
}

func (q *ClientQueue) TryDequeue() (*Client, bool) {
    q.mu.Lock()
    defer q.mu.Unlock()
    if len(q.clients) == 0 {
        return nil, false
    }
    client := q.clients[0]
    q.clients = q.clients[1:]
    return client, true
}

func (q *ClientQueue) Remove(client *Client) {
    q.mu.Lock()
    defer q.mu.Unlock()
    for i, c := range q.clients {
        if c == client {
            q.clients = append(q.clients[:i], q.clients[i+1:]...)
            return
        }
    }
}

对应的handleFindPeer中创建客户端时改为指针:

client := &Client{
    addr:          r.RemoteAddr,
    respWriter:    w,
    peerMatchedCh: make(chan struct{}),
}

内容的提问来源于stack exchange,提问作者Diwas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 03:02:18