fix(notify): 修复传输生命周期竞态,完善背压与协议边界

- 完善 stream/bulk DataID 分配、预留和双向命名空间,修复并发打开及 dedicated/shared 回退时的 ID 冲突
- 将收发、回复、恢复任务和 sidecar 绑定原始会话与物理连接,防止重连后的旧消息误操作新连接
- 加强 close/reset 身份校验及实例移除检查,修复 dedicated attach 失败、通道引用和资源回收竞态
- 收紧批量发送器停止准入,确保在途入队完成后统一清理请求、缓冲区和等待者
- 修复 record 满队列死锁、取消时序号消耗及关闭竞态,确保关闭有界并返回真实错误
- 增加协商式 record 逻辑半关闭,保留反向 ACK;通过 reset 传递 RecordFailure,避免背压掩盖原始失败原因
- 补齐帧长度、批次数量、序号溢出和未确认窗口校验,提前拒绝超限数据并按字节预算拆批
- 为入站分发增加全局及单连接的条数、字节预算和阻塞背压,关闭时唤醒等待者,消除正常断连日志噪音
- 完善 bulk 窗口释放失败处理与传输诊断,补充并发、重连、背压、协议边界及真实 TCP 回归覆盖
This commit is contained in:
2026-09-23 15:33:17 +08:00
parent 0826e17063
commit 1f2e74acca
79 changed files with 9190 additions and 1013 deletions
+184
View File
@@ -0,0 +1,184 @@
package notify
import (
"context"
"io"
"time"
)
const defaultRecordCloseTimeout = 5 * time.Second
type recordCloseMode uint8
const (
recordCloseNone recordCloseMode = iota
recordCloseWrite
recordCloseFull
)
func (r *recordStream) CloseWrite() error {
if r == nil {
return errRecordStreamNil
}
return r.closeRecord(recordCloseWrite)
}
func (r *recordStream) Close() error {
if r == nil {
return nil
}
return r.closeRecord(recordCloseFull)
}
func (r *recordStream) closeRecord(mode recordCloseMode) error {
r.closeMu.Lock()
defer r.closeMu.Unlock()
if r.closeDone {
return r.closeErr
}
if mode == recordCloseWrite && r.halfClosed {
return nil
}
r.mu.Lock()
r.outboundClosed = true
target := r.enqueuedOutboundSeq
err := r.streamErrorLocked()
r.signalStateLocked()
r.mu.Unlock()
if err != nil {
r.abortUnderlyingStream(err)
r.closeDone, r.closeErr = true, err
return err
}
timeout := r.cfg.CloseTimeout
if timeout <= 0 {
timeout = defaultRecordCloseTimeout
}
// A successful underlying Close can itself cancel the stream context.
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err = r.flushAndClose(ctx, target, mode)
if err != nil {
r.setTerminalError(err)
r.abortUnderlyingStream(err)
r.closeDone, r.closeErr = true, err
return err
}
if mode == recordCloseFull {
r.setTerminalError(io.ErrClosedPipe)
r.closeDone = true
} else {
r.halfClosed = true
}
return nil
}
func (r *recordStream) flushAndClose(ctx context.Context, target uint64, mode recordCloseMode) error {
req := recordFlushRequest{
ctx: ctx, targetSeq: target, forceAck: true, closeMode: mode,
done: make(chan error, 1),
}
select {
case <-r.ctx.Done():
return r.streamError()
case <-ctx.Done():
return ctx.Err()
case r.flushCh <- req:
}
select {
case err := <-req.done:
return err
case <-r.writerCh:
select {
case err := <-req.done:
return err
default:
return r.streamError()
}
case <-ctx.Done():
select {
case err := <-req.done:
return err
default:
return ctx.Err()
}
}
}
// The writer serializes the last data, final ACK and close control message.
func (r *recordStream) closeUnderlyingFromWriter(req recordFlushRequest) error {
if err := req.ctx.Err(); err != nil {
return err
}
if req.closeMode == recordCloseWrite && r.useHalfClose {
return r.writePayloadFrame(encodeRecordFINFrame(req.targetSeq))
}
deadline, _ := req.ctx.Deadline()
if err := r.stream.SetWriteDeadline(deadline); err != nil {
return err
}
if req.closeMode == recordCloseFull {
return r.stream.Close()
}
return r.stream.CloseWrite()
}
func (r *recordStream) abortUnderlyingStream(err error) {
if stream, ok := r.stream.(*streamHandle); ok {
stream.mu.Lock()
resetFn := stream.resetFn
stream.mu.Unlock()
// Local teardown must not wait for a reset reply from a stalled peer.
if !stream.applyResetState(err) {
return
}
if resetFn != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), defaultRecordCloseTimeout)
defer cancel()
_ = resetFn(ctx, stream, streamResetMessage(err))
}()
}
return
}
_ = r.stream.SetDeadline(time.Now())
_ = r.stream.Reset(err)
}
func (r *recordStream) abortRecord(err error) {
r.setTerminalError(err)
r.abortUnderlyingStream(err)
}
func (r *recordStream) abortProtocol(err error) {
r.setTerminalError(err)
_ = r.notifyFailureAndAbort(RecordFailure{
FailedSeq: r.nextInboundFailureSeq(),
Code: RecordErrorCodeProtocol,
Message: err.Error(),
})
}
func (r *recordStream) abortTimeout() time.Duration {
if r.cfg.CloseTimeout > 0 && r.cfg.CloseTimeout < defaultRecordCloseTimeout {
return r.cfg.CloseTimeout
}
return defaultRecordCloseTimeout
}
func (r *recordStream) closeReceive() {
r.recvCloseOnce.Do(func() { close(r.recvCh) })
}
func (r *recordStream) receiveFIN(finalSeq uint64) error {
r.mu.Lock()
if !r.useHalfClose || r.inboundClosed || finalSeq != r.inboundReceivedSeq {
r.mu.Unlock()
return errRecordSeqInvalid
}
r.inboundClosed = true
r.mu.Unlock()
// The reader owns recvCh; it keeps consuming control frames after data EOF.
r.closeReceive()
return nil
}