fix(notify): 根治低带宽控制面阻塞与传输写入卡死

- 为控制消息增加优先级、公平调度、队列字节预算和自适应批处理
- 支持可取消的写门等待,收紧 shared/dedicated bulk、stream 和 Reply 写入边界
- 修复 bulk reset/close、连接 handoff 和安全 profile 切换时序
- 保留旧取消与超时错误契约,新增阶段化 TransportSendError
- 增加 ReplyCtx、ReplyObjCtx、写超时配置及黑洞连接和竞态回归测试
This commit is contained in:
2026-08-14 10:16:37 +08:00
parent 98ef9e7fcc
commit 0826e17063
45 changed files with 4231 additions and 293 deletions
+99 -21
View File
@@ -11,6 +11,10 @@ import (
"time"
)
// A zero public WriteTimeout preserves the legacy option contract, but a
// physical bulk write still needs a finite bound so a black-hole peer cannot
// hold the async worker (and CloseWrite) forever. The default is constant so
// concurrent bulk operations cannot observe a process-wide timeout change.
const (
BulkOpenSignalKey = "notify.bulk.open"
BulkCloseSignalKey = "notify.bulk.close"
@@ -26,6 +30,8 @@ const (
defaultBulkControlReadTimeout = 0
defaultBulkControlWriteTimeout = 0
defaultBulkAcceptReadyTimeout = 10 * time.Second
defaultBulkResetNotifyTimeout = 30 * time.Second
defaultBulkDataWriteTimeout = 2 * time.Minute
)
type BulkMetadata map[string]string
@@ -1278,9 +1284,13 @@ func (b *bulkHandle) close(full bool) error {
return nil
}
closeFn := b.closeFn
writeTimeout := b.writeTimeout
b.mu.Unlock()
if closeFn != nil && !b.dedicatedWriteHalfClosedSnapshot() {
if err := closeFn(context.Background(), b, true); err != nil && !errors.Is(err, errBulkNotFound) && !b.canIgnoreDedicatedCloseSendError(err) {
closeCtx, cancel := b.closeContext(writeTimeout)
err := closeFn(closeCtx, b, true)
cancel()
if err != nil && !errors.Is(err, errBulkNotFound) && !b.canIgnoreDedicatedCloseSendError(err) {
return err
}
}
@@ -1302,12 +1312,19 @@ func (b *bulkHandle) close(full bool) error {
return nil
}
closeFn := b.closeFn
writeTimeout := b.writeTimeout
b.mu.Unlock()
if err := b.waitPendingAsyncWrites(context.Background()); err != nil {
drainCtx, drainCancel := b.closeContext(writeTimeout)
err := b.waitPendingAsyncWrites(drainCtx)
drainCancel()
if err != nil {
return err
}
if closeFn != nil {
if err := closeFn(context.Background(), b, full); err != nil && !errors.Is(err, errBulkNotFound) && !b.canIgnoreDedicatedCloseSendError(err) {
closeCtx, cancel := b.closeContext(writeTimeout)
err := closeFn(closeCtx, b, full)
cancel()
if err != nil && !errors.Is(err, errBulkNotFound) && !b.canIgnoreDedicatedCloseSendError(err) {
return err
}
}
@@ -1348,13 +1365,29 @@ func (b *bulkHandle) Reset(err error) error {
}
resetFn := b.resetFn
b.mu.Unlock()
if resetFn != nil {
if sendErr := resetFn(context.Background(), b, bulkResetMessage(resetErr)); sendErr != nil {
return sendErr
}
if !b.applyResetState(resetErr) {
return b.resetErrSnapshot()
}
defer b.finalize()
if resetFn == nil {
return nil
}
timeout := defaultBulkResetNotifyTimeout
if b.writeTimeout > 0 && b.writeTimeout < timeout {
timeout = b.writeTimeout
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
done := make(chan error, 1)
go func() {
done <- resetFn(ctx, b, bulkResetMessage(resetErr))
}()
select {
case sendErr := <-done:
return sendErr
case <-ctx.Done():
return ctx.Err()
}
b.markReset(resetErr)
return nil
}
func (b *bulkHandle) Snapshot() BulkSnapshot {
@@ -1396,18 +1429,34 @@ func (b *bulkHandle) markReset(err error) {
if b == nil {
return
}
resetErr := bulkResetError(err)
b.mu.Lock()
if b.resetErr == nil {
b.resetErr = resetErr
b.clearBufferedDataLocked()
b.closeWriteStateLocked()
b.applyResetState(bulkResetError(err))
b.finalize()
}
func (b *bulkHandle) applyResetState(resetErr error) bool {
if b == nil {
return false
}
b.mu.Lock()
if b.resetErr != nil {
b.notifyFlowLocked()
b.mu.Unlock()
return false
}
b.resetErr = bulkResetError(resetErr)
b.clearBufferedDataLocked()
b.closeWriteStateLocked()
b.notifyFlowLocked()
b.mu.Unlock()
b.markAcceptReady(resetErr)
b.markAcceptReady(b.resetErrSnapshot())
b.notifyReadable()
b.finalize()
if b.cancel != nil {
b.cancel()
}
if b.writeCtxCancel != nil {
b.writeCtxCancel()
}
return true
}
func (b *bulkHandle) pushChunk(chunk []byte) error {
@@ -1818,7 +1867,12 @@ func (b *bulkHandle) finalize() {
if b.writeCtxCancel != nil {
b.writeCtxCancel()
}
if sender := b.clearDedicatedSender(); sender != nil {
sender := b.clearDedicatedSender()
conn, owned := b.clearDedicatedConn()
if conn != nil && owned {
_ = conn.Close()
}
if sender != nil {
sender.stop()
}
if b.client != nil && b.releaseDedicatedActiveReserved() {
@@ -1827,9 +1881,6 @@ func (b *bulkHandle) finalize() {
if b.client != nil {
b.client.releaseBulkDedicatedLane(b.dedicatedLaneIDSnapshot())
}
if conn, owned := b.clearDedicatedConn(); conn != nil && owned {
_ = conn.Close()
}
if b.runtime != nil {
b.runtime.remove(b.runtimeScope, b.id)
}
@@ -2296,6 +2347,33 @@ func bulkWriteContext(parent context.Context, timeout time.Duration) (context.Co
return ctx, cancel, nil
}
func bulkCloseContext(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
if parent == nil {
parent = context.Background()
}
if timeout <= 0 {
return context.WithTimeout(parent, defaultBulkDataWriteTimeout)
}
return context.WithTimeout(parent, timeout)
}
func (b *bulkHandle) closeContext(timeout time.Duration) (context.Context, context.CancelFunc) {
if b == nil {
return bulkCloseContext(nil, timeout)
}
parent := b.Context()
b.mu.Lock()
gracefulPeerClose := b.remoteClosed || b.peerReadClosed
b.mu.Unlock()
if gracefulPeerClose {
// A peer half/full close cancels the bulk context as part of normal EOF
// delivery. Keep the final close notification alive so the peer can
// complete the protocol handshake, while retaining its write timeout.
parent = context.Background()
}
return bulkCloseContext(parent, timeout)
}
func normalizeBulkOpenRequest(req BulkOpenRequest) BulkOpenRequest {
req.Range = normalizeBulkRange(req.Range)
req.Metadata = cloneBulkMetadata(req.Metadata)