fix(notify): 根治低带宽控制面阻塞与传输写入卡死
- 为控制消息增加优先级、公平调度、队列字节预算和自适应批处理 - 支持可取消的写门等待,收紧 shared/dedicated bulk、stream 和 Reply 写入边界 - 修复 bulk reset/close、连接 handoff 和安全 profile 切换时序 - 保留旧取消与超时错误契约,新增阶段化 TransportSendError - 增加 ReplyCtx、ReplyObjCtx、写超时配置及黑洞连接和竞态回归测试
This commit is contained in:
+733
-78
@@ -1,152 +1,727 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const controlBatchMaxPayloads = 16
|
||||
const (
|
||||
controlBatchMaxPayloads = 16
|
||||
controlBatchMaxPayloadBytes = 32 * 1024 * 1024
|
||||
controlBatchMaxQueuedBytes = 32 * 1024 * 1024
|
||||
controlBatchCriticalReservedBytes = 1 * 1024 * 1024
|
||||
controlBatchMaxCriticalBurst = 16
|
||||
)
|
||||
|
||||
type controlPriority uint8
|
||||
|
||||
const (
|
||||
controlPriorityNormal controlPriority = iota
|
||||
controlPriorityCritical
|
||||
)
|
||||
|
||||
const (
|
||||
controlBatchRequestQueued int32 = iota
|
||||
controlBatchRequestStarted
|
||||
controlBatchRequestCanceled
|
||||
)
|
||||
|
||||
type controlBatchRequestState struct {
|
||||
value atomic.Int32
|
||||
}
|
||||
|
||||
type controlBatchRequest struct {
|
||||
payload []byte
|
||||
deadline time.Time
|
||||
done chan error
|
||||
ctx context.Context
|
||||
payload []byte
|
||||
writeTime time.Duration
|
||||
priority controlPriority
|
||||
done chan error
|
||||
state *controlBatchRequestState
|
||||
queueSize int64
|
||||
}
|
||||
|
||||
type controlBatchSender struct {
|
||||
binding *transportBinding
|
||||
reqCh chan controlBatchRequest
|
||||
stopCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
binding *transportBinding
|
||||
normalCh chan controlBatchRequest
|
||||
criticalCh chan controlBatchRequest
|
||||
stopCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
budgetWake chan struct{}
|
||||
stopCtx context.Context
|
||||
stopCancel context.CancelFunc
|
||||
|
||||
stopOnce sync.Once
|
||||
errMu sync.Mutex
|
||||
err error
|
||||
stopOnce sync.Once
|
||||
flushMu sync.Mutex
|
||||
admissionMu sync.Mutex
|
||||
admitting sync.WaitGroup
|
||||
admissionClosed bool
|
||||
queued atomic.Int64
|
||||
queuedSize atomic.Int64
|
||||
errMu sync.Mutex
|
||||
err error
|
||||
}
|
||||
|
||||
func newControlBatchSender(binding *transportBinding) *controlBatchSender {
|
||||
stopCtx, stopCancel := context.WithCancel(context.Background())
|
||||
sender := &controlBatchSender{
|
||||
binding: binding,
|
||||
reqCh: make(chan controlBatchRequest, controlBatchMaxPayloads*4),
|
||||
stopCh: make(chan struct{}),
|
||||
doneCh: make(chan struct{}),
|
||||
binding: binding,
|
||||
normalCh: make(chan controlBatchRequest, controlBatchMaxPayloads*4),
|
||||
criticalCh: make(chan controlBatchRequest, controlBatchMaxPayloads*2),
|
||||
stopCh: make(chan struct{}),
|
||||
doneCh: make(chan struct{}),
|
||||
budgetWake: make(chan struct{}, 1),
|
||||
stopCtx: stopCtx,
|
||||
stopCancel: stopCancel,
|
||||
}
|
||||
go sender.run()
|
||||
return sender
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) submit(payload []byte, deadline time.Time) error {
|
||||
func (s *controlBatchSender) submit(payload []byte, writeTimeout time.Duration) error {
|
||||
return s.submitContext(context.Background(), payload, writeTimeout, controlPriorityNormal)
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) submitContext(ctx context.Context, payload []byte, writeTimeout time.Duration, priority controlPriority) error {
|
||||
if s == nil {
|
||||
return errTransportDetached
|
||||
return newTransportSendError(TransportSendStageTransport, errTransportDetached)
|
||||
}
|
||||
req := controlBatchRequest{
|
||||
payload: payload,
|
||||
deadline: deadline,
|
||||
done: make(chan error, 1),
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := s.errSnapshot(); err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-s.stopCh:
|
||||
return s.stoppedErr()
|
||||
case s.reqCh <- req:
|
||||
if err := ctx.Err(); err != nil {
|
||||
return newTransportSendError(TransportSendStageQueue, err)
|
||||
}
|
||||
return <-req.done
|
||||
if len(payload) > controlBatchMaxPayloadBytes {
|
||||
return newTransportSendError(
|
||||
TransportSendStageQueue,
|
||||
fmt.Errorf("control payload is %d bytes, maximum is %d; use stream or bulk transfer", len(payload), controlBatchMaxPayloadBytes),
|
||||
)
|
||||
}
|
||||
req := controlBatchRequest{
|
||||
ctx: ctx,
|
||||
payload: payload,
|
||||
writeTime: maxDuration(0, writeTimeout),
|
||||
priority: priority,
|
||||
queueSize: int64(maxInt(1, len(payload))),
|
||||
}
|
||||
if submitted, err := s.tryDirectSubmit(req); submitted {
|
||||
return err
|
||||
}
|
||||
queueCtx, cancelQueue := contextWithTimeoutUpperBound(ctx, req.writeTime)
|
||||
defer cancelQueue()
|
||||
req.ctx = queueCtx
|
||||
if req.ctx.Done() != nil {
|
||||
req.payload = bytes.Clone(payload)
|
||||
}
|
||||
|
||||
s.queued.Add(1)
|
||||
if err := s.reserveQueueBytes(req.ctx, req.queueSize, priority); err != nil {
|
||||
s.queued.Add(-1)
|
||||
return err
|
||||
}
|
||||
req.done = make(chan error, 1)
|
||||
req.state = &controlBatchRequestState{}
|
||||
queued := false
|
||||
defer func() {
|
||||
if !queued {
|
||||
s.queued.Add(-1)
|
||||
s.releaseQueueBytes(req.queueSize)
|
||||
}
|
||||
}()
|
||||
if !s.beginAdmission() {
|
||||
return s.stoppedErr()
|
||||
}
|
||||
select {
|
||||
case <-req.ctx.Done():
|
||||
s.endAdmission()
|
||||
return newTransportSendError(TransportSendStageQueue, req.ctx.Err())
|
||||
case <-s.stopCh:
|
||||
s.endAdmission()
|
||||
return s.stoppedErr()
|
||||
case s.requestChannel(priority) <- req:
|
||||
queued = true
|
||||
s.endAdmission()
|
||||
}
|
||||
select {
|
||||
case err := <-req.done:
|
||||
return err
|
||||
case <-s.stopCh:
|
||||
if req.tryCancel() {
|
||||
return s.stoppedErr()
|
||||
}
|
||||
if req.ctx != nil && req.ctx.Done() != nil {
|
||||
return s.stoppedErr()
|
||||
}
|
||||
return <-req.done
|
||||
case <-req.ctx.Done():
|
||||
if req.tryCancel() {
|
||||
return newTransportSendError(TransportSendStageQueue, req.ctx.Err())
|
||||
}
|
||||
return newTransportSendError(TransportSendStageQueue, req.ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func contextWithTimeoutUpperBound(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return ctx, func() {}
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
if current, ok := ctx.Deadline(); ok && !deadline.Before(current) {
|
||||
return ctx, func() {}
|
||||
}
|
||||
return context.WithDeadline(ctx, deadline)
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) tryDirectSubmit(req controlBatchRequest) (bool, error) {
|
||||
if s == nil {
|
||||
return true, newTransportSendError(TransportSendStageTransport, errTransportDetached)
|
||||
}
|
||||
if err := s.errSnapshot(); err != nil {
|
||||
return true, err
|
||||
}
|
||||
select {
|
||||
case <-req.ctx.Done():
|
||||
return true, newTransportSendError(TransportSendStageQueue, req.ctx.Err())
|
||||
case <-s.stopCh:
|
||||
return true, s.stoppedErr()
|
||||
default:
|
||||
}
|
||||
if req.ctx.Done() != nil {
|
||||
return false, nil
|
||||
}
|
||||
if s.queued.Load() != 0 || !s.flushMu.TryLock() {
|
||||
return false, nil
|
||||
}
|
||||
defer s.flushMu.Unlock()
|
||||
if s.queued.Load() != 0 {
|
||||
return false, nil
|
||||
}
|
||||
if err := s.errSnapshot(); err != nil {
|
||||
return true, err
|
||||
}
|
||||
if err := s.flushDirect(req); err != nil {
|
||||
if stage, ok := TransportSendErrorStage(err); ok && stage == TransportSendStageQueue {
|
||||
return true, err
|
||||
}
|
||||
s.markFailed(err)
|
||||
s.waitAdmissions()
|
||||
s.failPending(err, nil, nil)
|
||||
return true, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) run() {
|
||||
defer close(s.doneCh)
|
||||
var pendingNormal []controlBatchRequest
|
||||
var pendingCritical []controlBatchRequest
|
||||
criticalBurst := 0
|
||||
for {
|
||||
req, ok := s.nextRequest()
|
||||
forceNormal := criticalBurst >= controlBatchMaxCriticalBurst
|
||||
req, ok := s.nextRequest(&pendingNormal, &pendingCritical, forceNormal)
|
||||
if !ok {
|
||||
s.waitAdmissions()
|
||||
s.failPending(s.stoppedErr(), pendingNormal, pendingCritical)
|
||||
return
|
||||
}
|
||||
normalWaiting := s.hasNormalRequest(&pendingNormal)
|
||||
if req.priority == controlPriorityCritical {
|
||||
switch {
|
||||
case normalWaiting && criticalBurst >= controlBatchMaxCriticalBurst:
|
||||
pushControlPendingFront(req, &pendingNormal, &pendingCritical)
|
||||
var available bool
|
||||
req, available = s.tryNextRequest(controlPriorityNormal, &pendingNormal, &pendingCritical)
|
||||
if !available {
|
||||
s.waitAdmissions()
|
||||
s.failPending(s.stoppedErr(), pendingNormal, pendingCritical)
|
||||
return
|
||||
}
|
||||
forceNormal = true
|
||||
case !normalWaiting:
|
||||
criticalBurst = 0
|
||||
}
|
||||
}
|
||||
batch := []controlBatchRequest{req}
|
||||
drain:
|
||||
for len(batch) < controlBatchMaxPayloads {
|
||||
select {
|
||||
case <-s.stopCh:
|
||||
s.failPending(s.stoppedErr())
|
||||
return
|
||||
case next := <-s.reqCh:
|
||||
batch = append(batch, next)
|
||||
default:
|
||||
break drain
|
||||
batchBytes := len(req.payload)
|
||||
softLimit := s.batchSoftPayloadLimit()
|
||||
batchLimit := controlBatchMaxPayloads
|
||||
if req.priority == controlPriorityCritical && normalWaiting {
|
||||
remaining := controlBatchMaxCriticalBurst - criticalBurst
|
||||
if remaining < batchLimit {
|
||||
batchLimit = remaining
|
||||
}
|
||||
}
|
||||
payloads := make([][]byte, 0, len(batch))
|
||||
for _, item := range batch {
|
||||
payloads = append(payloads, item.payload)
|
||||
for len(batch) < batchLimit {
|
||||
next, available := s.tryNextRequest(req.priority, &pendingNormal, &pendingCritical)
|
||||
if !available {
|
||||
break
|
||||
}
|
||||
if !controlBatchCanAppend(batchBytes, len(next.payload), softLimit) {
|
||||
pushControlPendingFront(next, &pendingNormal, &pendingCritical)
|
||||
break
|
||||
}
|
||||
batch = append(batch, next)
|
||||
batchBytes += len(next.payload)
|
||||
}
|
||||
err := s.flush(payloads, controlBatchRequestsEarliestDeadline(batch))
|
||||
|
||||
s.flushMu.Lock()
|
||||
if req.priority == controlPriorityNormal && !forceNormal && criticalBurst < controlBatchMaxCriticalBurst {
|
||||
if critical, available := s.tryNextRequest(controlPriorityCritical, &pendingNormal, &pendingCritical); available {
|
||||
pendingNormal = append(append(make([]controlBatchRequest, 0, len(batch)+len(pendingNormal)), batch...), pendingNormal...)
|
||||
batch = []controlBatchRequest{critical}
|
||||
batchBytes = len(critical.payload)
|
||||
batchLimit = controlBatchMaxCriticalBurst - criticalBurst
|
||||
if batchLimit > controlBatchMaxPayloads {
|
||||
batchLimit = controlBatchMaxPayloads
|
||||
}
|
||||
for len(batch) < batchLimit {
|
||||
next, ok := s.tryNextRequest(controlPriorityCritical, &pendingNormal, &pendingCritical)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if !controlBatchCanAppend(batchBytes, len(next.payload), softLimit) {
|
||||
pushControlPendingFront(next, &pendingNormal, &pendingCritical)
|
||||
break
|
||||
}
|
||||
batch = append(batch, next)
|
||||
batchBytes += len(next.payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
batchPriority := batch[0].priority
|
||||
err := s.flushQueued(batch)
|
||||
s.flushMu.Unlock()
|
||||
if err != nil {
|
||||
s.setErr(err)
|
||||
for _, item := range batch {
|
||||
item.done <- err
|
||||
}
|
||||
s.failPending(err)
|
||||
s.markFailed(err)
|
||||
s.waitAdmissions()
|
||||
s.failPending(err, pendingNormal, pendingCritical)
|
||||
return
|
||||
}
|
||||
for _, item := range batch {
|
||||
item.done <- nil
|
||||
if batchPriority == controlPriorityCritical {
|
||||
criticalBurst += len(batch)
|
||||
if criticalBurst > controlBatchMaxCriticalBurst {
|
||||
criticalBurst = controlBatchMaxCriticalBurst
|
||||
}
|
||||
} else {
|
||||
criticalBurst = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) nextRequest() (controlBatchRequest, bool) {
|
||||
select {
|
||||
case <-s.stopCh:
|
||||
s.failPending(s.stoppedErr())
|
||||
return controlBatchRequest{}, false
|
||||
case req := <-s.reqCh:
|
||||
func (s *controlBatchSender) nextRequest(pendingNormal *[]controlBatchRequest, pendingCritical *[]controlBatchRequest, forceNormal bool) (controlBatchRequest, bool) {
|
||||
if forceNormal {
|
||||
if req, ok := popControlPending(pendingNormal); ok {
|
||||
return req, true
|
||||
}
|
||||
select {
|
||||
case req := <-s.normalCh:
|
||||
return req, true
|
||||
default:
|
||||
}
|
||||
}
|
||||
if req, ok := popControlPending(pendingCritical); ok {
|
||||
return req, true
|
||||
}
|
||||
select {
|
||||
case req := <-s.criticalCh:
|
||||
return req, true
|
||||
default:
|
||||
}
|
||||
if req, ok := popControlPending(pendingNormal); ok {
|
||||
return req, true
|
||||
}
|
||||
select {
|
||||
case <-s.stopCh:
|
||||
return controlBatchRequest{}, false
|
||||
case req := <-s.criticalCh:
|
||||
return req, true
|
||||
case req := <-s.normalCh:
|
||||
return req, true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) tryNextRequest(priority controlPriority, pendingNormal *[]controlBatchRequest, pendingCritical *[]controlBatchRequest) (controlBatchRequest, bool) {
|
||||
pending := pendingNormal
|
||||
if priority == controlPriorityCritical {
|
||||
pending = pendingCritical
|
||||
}
|
||||
if req, ok := popControlPending(pending); ok {
|
||||
return req, true
|
||||
}
|
||||
select {
|
||||
case <-s.stopCh:
|
||||
return controlBatchRequest{}, false
|
||||
case req := <-s.requestChannel(priority):
|
||||
return req, true
|
||||
default:
|
||||
return controlBatchRequest{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func popControlPending(pending *[]controlBatchRequest) (controlBatchRequest, bool) {
|
||||
if pending == nil || len(*pending) == 0 {
|
||||
return controlBatchRequest{}, false
|
||||
}
|
||||
req := (*pending)[0]
|
||||
*pending = (*pending)[1:]
|
||||
return req, true
|
||||
}
|
||||
|
||||
func pushControlPendingFront(req controlBatchRequest, pendingNormal *[]controlBatchRequest, pendingCritical *[]controlBatchRequest) {
|
||||
pending := pendingNormal
|
||||
if req.priority == controlPriorityCritical {
|
||||
pending = pendingCritical
|
||||
}
|
||||
*pending = append([]controlBatchRequest{req}, (*pending)...)
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) requestChannel(priority controlPriority) chan controlBatchRequest {
|
||||
if priority == controlPriorityCritical {
|
||||
return s.criticalCh
|
||||
}
|
||||
return s.normalCh
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) hasNormalRequest(pendingNormal *[]controlBatchRequest) bool {
|
||||
if pendingNormal != nil && len(*pendingNormal) > 0 {
|
||||
return true
|
||||
}
|
||||
return s != nil && len(s.normalCh) > 0
|
||||
}
|
||||
|
||||
func controlBatchCanAppend(batchBytes int, nextBytes int, softLimit int) bool {
|
||||
if batchBytes == 0 {
|
||||
return true
|
||||
}
|
||||
if softLimit <= 0 {
|
||||
return false
|
||||
}
|
||||
return nextBytes <= softLimit-batchBytes
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) batchSoftPayloadLimit() int {
|
||||
if s == nil || s.binding == nil {
|
||||
return controlAdaptiveSoftPayloadFallbackBytes
|
||||
}
|
||||
return s.binding.controlAdaptiveSoftPayloadBytesSnapshot()
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) reserveQueueBytes(ctx context.Context, size int64, priority controlPriority) error {
|
||||
limit := int64(controlBatchMaxQueuedBytes)
|
||||
if priority == controlPriorityCritical {
|
||||
limit += int64(controlBatchCriticalReservedBytes)
|
||||
}
|
||||
for {
|
||||
current := s.queuedSize.Load()
|
||||
if (current == 0 && size > limit) || size <= limit-current {
|
||||
if s.queuedSize.CompareAndSwap(current, current+size) {
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return newTransportSendError(TransportSendStageQueue, ctx.Err())
|
||||
case <-s.stopCh:
|
||||
return s.stoppedErr()
|
||||
case <-s.budgetWake:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) releaseQueueBytes(size int64) {
|
||||
if s == nil || size <= 0 {
|
||||
return
|
||||
}
|
||||
s.queuedSize.Add(-size)
|
||||
select {
|
||||
case s.budgetWake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) flushDirect(req controlBatchRequest) error {
|
||||
if s == nil || s.binding == nil {
|
||||
return newTransportSendError(TransportSendStageTransport, errTransportDetached)
|
||||
}
|
||||
queue := s.binding.queueSnapshot()
|
||||
if queue == nil {
|
||||
return newTransportSendError(TransportSendStageTransport, errTransportFrameQueueUnavailable)
|
||||
}
|
||||
var preWriteErr error
|
||||
didWrite := false
|
||||
started := time.Now()
|
||||
lockAcquired, err := s.binding.withConnWriteLockContextStopDeadlineManaged(req.ctx, s.stopCh, writeDeadlineFromTimeout(req.writeTime), func(conn net.Conn) error {
|
||||
if stoppedErr := s.errSnapshot(); stoppedErr != nil {
|
||||
preWriteErr = stoppedErr
|
||||
return nil
|
||||
}
|
||||
if ctxErr := req.contextErr(); ctxErr != nil {
|
||||
preWriteErr = ctxErr
|
||||
return nil
|
||||
}
|
||||
didWrite = true
|
||||
return writeFramedPayloadBatchUnlocked(conn, queue, [][]byte{req.payload})
|
||||
})
|
||||
if preWriteErr != nil {
|
||||
return preWriteErr
|
||||
}
|
||||
if !lockAcquired {
|
||||
if ctxErr := req.contextErr(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
if stoppedErr := s.errSnapshot(); stoppedErr != nil {
|
||||
return stoppedErr
|
||||
}
|
||||
return newTransportSendError(TransportSendStageTransport, err)
|
||||
}
|
||||
if didWrite {
|
||||
s.binding.observeControlAdaptivePayloadWrite(len(req.payload), time.Since(started), req.writeTime, err)
|
||||
}
|
||||
if err != nil {
|
||||
if lockAcquired {
|
||||
// A framed write may have left a partial frame on the wire. Do not
|
||||
// let a later request reuse this connection.
|
||||
s.binding.closeConn()
|
||||
}
|
||||
return newTransportSendError(TransportSendStageWrite, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) flushQueued(requests []controlBatchRequest) error {
|
||||
if s == nil || s.binding == nil {
|
||||
err := newTransportSendError(TransportSendStageTransport, errTransportDetached)
|
||||
s.finishRequests(requests, err)
|
||||
return err
|
||||
}
|
||||
queue := s.binding.queueSnapshot()
|
||||
if queue == nil {
|
||||
err := newTransportSendError(TransportSendStageTransport, errTransportFrameQueueUnavailable)
|
||||
s.finishRequests(requests, err)
|
||||
return err
|
||||
}
|
||||
pending := requests
|
||||
for len(pending) > 0 {
|
||||
pending = s.finishCanceledRequests(pending)
|
||||
if len(pending) == 0 {
|
||||
return nil
|
||||
}
|
||||
if stoppedErr := s.errSnapshot(); stoppedErr != nil {
|
||||
s.finishRequests(pending, stoppedErr)
|
||||
return stoppedErr
|
||||
}
|
||||
|
||||
waitCtx, cleanupWait := s.controlBatchWaitContext(pending)
|
||||
active := make([]controlBatchRequest, 0, len(pending))
|
||||
var preWriteErr error
|
||||
payloadBytes := 0
|
||||
didWrite := false
|
||||
started := time.Now()
|
||||
writeDeadline := earlierWriteDeadline(
|
||||
writeDeadlineFromTimeout(controlBatchRequestsShortestWriteTimeout(pending)),
|
||||
controlBatchRequestsEarliestDeadline(pending),
|
||||
)
|
||||
lockAcquired, err := s.binding.withConnWriteLockContextStopDeadlineManaged(waitCtx, s.stopCh, writeDeadline, func(conn net.Conn) error {
|
||||
if stoppedErr := s.errSnapshot(); stoppedErr != nil {
|
||||
preWriteErr = stoppedErr
|
||||
return nil
|
||||
}
|
||||
for _, item := range pending {
|
||||
if cancelErr, canceled := item.cancelBeforeStart(); canceled {
|
||||
s.finishRequest(item, cancelErr)
|
||||
continue
|
||||
}
|
||||
if !item.tryStart() {
|
||||
s.finishRequest(item, item.canceledErr())
|
||||
continue
|
||||
}
|
||||
active = append(active, item)
|
||||
payloadBytes += len(item.payload)
|
||||
}
|
||||
if len(active) == 0 {
|
||||
return nil
|
||||
}
|
||||
payloads := make([][]byte, 0, len(active))
|
||||
for _, item := range active {
|
||||
payloads = append(payloads, item.payload)
|
||||
}
|
||||
didWrite = true
|
||||
return writeFramedPayloadBatchUnlocked(conn, queue, payloads)
|
||||
})
|
||||
cleanupWait()
|
||||
if preWriteErr != nil {
|
||||
s.finishRequests(pending, preWriteErr)
|
||||
return preWriteErr
|
||||
}
|
||||
if !lockAcquired {
|
||||
remaining := s.finishCanceledRequests(pending)
|
||||
if len(remaining) < len(pending) {
|
||||
pending = remaining
|
||||
continue
|
||||
}
|
||||
if stoppedErr := s.errSnapshot(); stoppedErr != nil {
|
||||
s.finishRequests(pending, stoppedErr)
|
||||
return stoppedErr
|
||||
}
|
||||
transportErr := newTransportSendError(TransportSendStageTransport, err)
|
||||
s.finishRequests(pending, transportErr)
|
||||
return transportErr
|
||||
}
|
||||
if len(active) == 0 {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
writeErr := newTransportSendError(TransportSendStageWrite, err)
|
||||
s.finishRequests(pending, writeErr)
|
||||
return writeErr
|
||||
}
|
||||
writeTimeout := controlBatchRequestsShortestWriteTimeout(active)
|
||||
if didWrite {
|
||||
s.binding.observeControlAdaptivePayloadWrite(payloadBytes, time.Since(started), writeTimeout, err)
|
||||
}
|
||||
if err != nil {
|
||||
if lockAcquired {
|
||||
// A framed write may have left a partial frame on the wire. Do not
|
||||
// let a later request reuse this connection.
|
||||
s.binding.closeConn()
|
||||
}
|
||||
err = newTransportSendError(TransportSendStageWrite, err)
|
||||
}
|
||||
s.finishRequests(active, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) controlBatchWaitContext(requests []controlBatchRequest) (context.Context, func()) {
|
||||
base := context.Background()
|
||||
if s != nil && s.stopCtx != nil {
|
||||
base = s.stopCtx
|
||||
}
|
||||
ctx, cancel := context.WithCancel(base)
|
||||
stops := make([]func() bool, 0, len(requests))
|
||||
for _, item := range requests {
|
||||
if item.ctx == nil || item.ctx.Done() == nil {
|
||||
continue
|
||||
}
|
||||
stops = append(stops, context.AfterFunc(item.ctx, cancel))
|
||||
}
|
||||
return ctx, func() {
|
||||
for _, stop := range stops {
|
||||
stop()
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func controlBatchRequestsShortestWriteTimeout(batch []controlBatchRequest) time.Duration {
|
||||
var timeout time.Duration
|
||||
for _, item := range batch {
|
||||
if item.writeTime <= 0 {
|
||||
continue
|
||||
}
|
||||
if timeout == 0 || item.writeTime < timeout {
|
||||
timeout = item.writeTime
|
||||
}
|
||||
}
|
||||
return timeout
|
||||
}
|
||||
|
||||
func controlBatchRequestsEarliestDeadline(batch []controlBatchRequest) time.Time {
|
||||
var deadline time.Time
|
||||
for _, item := range batch {
|
||||
if item.deadline.IsZero() {
|
||||
continue
|
||||
}
|
||||
if deadline.IsZero() || item.deadline.Before(deadline) {
|
||||
deadline = item.deadline
|
||||
}
|
||||
candidate := contextDeadline(item.ctx)
|
||||
deadline = earlierWriteDeadline(deadline, candidate)
|
||||
}
|
||||
return deadline
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) flush(payloads [][]byte, deadline time.Time) error {
|
||||
if s == nil || s.binding == nil {
|
||||
return errTransportDetached
|
||||
func (s *controlBatchSender) finishCanceledRequests(requests []controlBatchRequest) []controlBatchRequest {
|
||||
remaining := requests[:0]
|
||||
for _, item := range requests {
|
||||
if cancelErr, canceled := item.cancelBeforeStart(); canceled {
|
||||
s.finishRequest(item, cancelErr)
|
||||
continue
|
||||
}
|
||||
remaining = append(remaining, item)
|
||||
}
|
||||
queue := s.binding.queueSnapshot()
|
||||
if queue == nil {
|
||||
return errTransportFrameQueueUnavailable
|
||||
return remaining
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) finishRequests(requests []controlBatchRequest, err error) {
|
||||
for _, item := range requests {
|
||||
s.finishRequest(item, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) finishRequest(req controlBatchRequest, err error) {
|
||||
if s != nil {
|
||||
s.queued.Add(-1)
|
||||
s.releaseQueueBytes(req.queueSize)
|
||||
}
|
||||
req.done <- err
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) beginAdmission() bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
s.admissionMu.Lock()
|
||||
defer s.admissionMu.Unlock()
|
||||
if s.admissionClosed {
|
||||
return false
|
||||
}
|
||||
s.admitting.Add(1)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) endAdmission() {
|
||||
if s != nil {
|
||||
s.admitting.Done()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) waitAdmissions() {
|
||||
if s != nil {
|
||||
s.admitting.Wait()
|
||||
}
|
||||
return s.binding.withConnWriteLockDeadline(deadline, func(conn net.Conn) error {
|
||||
return writeFramedPayloadBatchUnlocked(conn, queue, payloads)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) stop() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.stopOnce.Do(func() {
|
||||
s.setErr(errTransportDetached)
|
||||
close(s.stopCh)
|
||||
})
|
||||
s.markFailed(newTransportSendError(TransportSendStageTransport, errTransportDetached))
|
||||
<-s.doneCh
|
||||
s.flushMu.Lock()
|
||||
s.flushMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) failPending(err error) {
|
||||
func (s *controlBatchSender) failPending(err error, pendingNormal []controlBatchRequest, pendingCritical []controlBatchRequest) {
|
||||
for _, item := range pendingCritical {
|
||||
s.finishRequest(item, err)
|
||||
}
|
||||
for _, item := range pendingNormal {
|
||||
s.finishRequest(item, err)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case item := <-s.reqCh:
|
||||
item.done <- err
|
||||
case item := <-s.criticalCh:
|
||||
s.finishRequest(item, err)
|
||||
case item := <-s.normalCh:
|
||||
s.finishRequest(item, err)
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -164,9 +739,25 @@ func (s *controlBatchSender) setErr(err error) {
|
||||
s.errMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) markFailed(err error) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.setErr(err)
|
||||
s.stopOnce.Do(func() {
|
||||
s.admissionMu.Lock()
|
||||
s.admissionClosed = true
|
||||
if s.stopCancel != nil {
|
||||
s.stopCancel()
|
||||
}
|
||||
close(s.stopCh)
|
||||
s.admissionMu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *controlBatchSender) errSnapshot() error {
|
||||
if s == nil {
|
||||
return errTransportDetached
|
||||
return newTransportSendError(TransportSendStageTransport, errTransportDetached)
|
||||
}
|
||||
s.errMu.Lock()
|
||||
defer s.errMu.Unlock()
|
||||
@@ -177,5 +768,69 @@ func (s *controlBatchSender) stoppedErr() error {
|
||||
if err := s.errSnapshot(); err != nil {
|
||||
return err
|
||||
}
|
||||
return errTransportDetached
|
||||
return newTransportSendError(TransportSendStageTransport, errTransportDetached)
|
||||
}
|
||||
|
||||
func (r controlBatchRequest) tryStart() bool {
|
||||
if r.state == nil {
|
||||
return true
|
||||
}
|
||||
return r.state.value.CompareAndSwap(controlBatchRequestQueued, controlBatchRequestStarted)
|
||||
}
|
||||
|
||||
func (r controlBatchRequest) tryCancel() bool {
|
||||
return r.state != nil && r.state.value.CompareAndSwap(controlBatchRequestQueued, controlBatchRequestCanceled)
|
||||
}
|
||||
|
||||
func (r controlBatchRequest) cancelBeforeStart() (error, bool) {
|
||||
if r.state == nil {
|
||||
if err := r.contextErr(); err != nil {
|
||||
return err, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
if r.state.value.Load() == controlBatchRequestCanceled {
|
||||
return r.canceledErr(), true
|
||||
}
|
||||
if err := r.contextErr(); err != nil && r.tryCancel() {
|
||||
return err, true
|
||||
}
|
||||
if r.state.value.Load() == controlBatchRequestCanceled {
|
||||
return r.canceledErr(), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (r controlBatchRequest) contextErr() error {
|
||||
if r.ctx == nil {
|
||||
return nil
|
||||
}
|
||||
err := r.ctx.Err()
|
||||
if err == nil {
|
||||
if deadline, ok := r.ctx.Deadline(); ok && !time.Now().Before(deadline) {
|
||||
err = context.DeadlineExceeded
|
||||
}
|
||||
}
|
||||
return newTransportSendError(TransportSendStageQueue, err)
|
||||
}
|
||||
|
||||
func (r controlBatchRequest) canceledErr() error {
|
||||
if r.ctx != nil && r.ctx.Err() != nil {
|
||||
return newTransportSendError(TransportSendStageQueue, r.ctx.Err())
|
||||
}
|
||||
return newTransportSendError(TransportSendStageQueue, context.Canceled)
|
||||
}
|
||||
|
||||
func maxInt(left int, right int) int {
|
||||
if left > right {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
func maxDuration(left time.Duration, right time.Duration) time.Duration {
|
||||
if left > right {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user