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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user