owpengram-server/internal/mtprotoedge/outbound.go

1547 lines
46 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mtprotoedge
import (
"bufio"
"context"
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"errors"
"fmt"
"io"
"sync"
"sync/atomic"
"time"
"github.com/gotd/td/bin"
"github.com/gotd/td/crypto"
"github.com/gotd/td/mt"
"github.com/gotd/td/proto"
"telesrv/internal/compat/layerwire"
)
var (
// ErrConnClosed 表示连接的出站 actor 已关闭。
ErrConnClosed = errors.New("mtproto connection closed")
// ErrOutboundQueueFull 表示 best-effort update push 未能在预算内进入出站队列。
ErrOutboundQueueFull = errors.New("mtproto outbound queue full")
// ErrOutboundTrackedBudget 表示消息无法进入 Server 级 resend tracking 预算。可靠 RPC
// 响应会终止连接让客户端重试best-effort update 仅丢弃加速推送,由 difference 恢复。
ErrOutboundTrackedBudget = errors.New("mtproto outbound tracked byte budget exhausted")
ErrOutboundMessageTooLarge = errors.New("mtproto outbound message exceeds transport frame limit")
)
const (
// 原实现为每条 Conn eager 分配 1024 + 256 个 outboundOp 槽;在大连接数下仅空队列
// backing 就占据大量常驻内存。默认缩至 128 + 32仍覆盖 TDesktop 启动/推送突发,
// 慢消费者继续由 best-effort timeout + durable difference 降级。
defaultOutboundQueueSize = 128
defaultOutboundControlQueueSize = 32
defaultOutboundTrackedMaxBytes = int64(512 << 20) // 512 MiB / Server
defaultOutboundControlMaxBytes = int64(64 << 20) // ack/state/resend vectors / Server
maxTrackedServerMsgIDs = 4096
maxTrackedAckedMsgIDs = 1024
// maxTrackedServerBytes 是 pending已发送待 ack、用于 resend总 body 字节上限。
// 与 maxTrackedServerMsgIDs 并列:客户端从不 ack 时,大响应体按字节滚动丢弃,
// 防 pending 被「4096 条 × 大 body」撑爆。
maxTrackedServerBytes = 64 << 20 // 64 MiB
// Encrypted transport adds auth-key/msg-key, plaintext headers, randomized
// padding and codec framing. Reject before creating the two encryption buffers.
maxOutboundBodyBytes = maxTransportMessageSize - (2 << 10)
)
type outboundOpKind byte
const (
outboundSend outboundOpKind = iota + 1
outboundAck
outboundQueryState
outboundResend
outboundResendByRequest
)
type outboundOp struct {
kind outboundOpKind
control bool
ctx context.Context
msgType proto.MessageType
msg bin.Encoder
encoded *encodedOutboundMessage
// reservedBytes accounts for the encoded body while it is queued. For a
// content frame the reservation is transferred to resend tracking after a
// successful write; every other terminal path releases it exactly once.
reservedBytes int
reservationBudget *outboundTrackedBudget
ids []int64
reqMsgID int64
enqueuedAt time.Time
done chan outboundResult
}
type encodedOutboundMessage struct {
body []byte
typeID uint32
reqMsgID int64
}
type outboundResult struct {
info []byte
resent bool
err error
}
type outboundFrame struct {
msgID int64
seqNo int32
typeID uint32
body []byte
reservedBytes int
// reservationBudget follows this frame from producer queue through resend tracking.
// Service frames such as new_session_created are content-related and therefore pending,
// but their bytes must remain on the independent control budget for the full lifetime.
reservationBudget *outboundTrackedBudget
reqMsgID int64
sentAt time.Time
sends int
}
type outboundState struct {
pending map[int64]*outboundFrame
order []int64
byRequest map[int64]int64
acked map[int64]struct{}
ackOrder []int64
totalBytes int
maxMessages int
maxBytes int
budget *outboundTrackedBudget
}
// outboundTrackedBudget 是 body/control/write 三类预算共用的原子 byte-budget primitive。
// 每个实例只负责一类:普通 encoded body、encoded service frame + 控制向量,或 write
// scratch同一 reservation 不跨实例释放。
// 预算在入队前预留,写成功后从 queue reservation 原子转交给 pendingCAS 避免所有
// outbound producer/actor 在一把全局 mutex 上串行。
type outboundTrackedBudget struct {
maxBytes int64
used atomic.Int64
wakeMu sync.Mutex
wake *outboundBudgetWake
}
type outboundBudgetWake struct {
ch chan struct{}
waiters int
}
const defaultOutboundEncodeConcurrency = 32
// outboundEncodeSlots bounds the otherwise unaccounted transient allocation made by TL
// encoding and layer transcoding. The retained encoded bytes are covered by
// outboundTrackedBudget after Encode returns, but without this process-wide gate many RPC
// workers could all allocate a near-limit body before any of them attempted that reservation.
// Encoding cannot be cancelled once an Encoder has started, so admission is intentionally
// acquired before calling user/domain supplied Encoder code and released immediately after the
// transient allocation has either become a tracked body or been discarded.
var outboundEncodeSlots = make(chan struct{}, defaultOutboundEncodeConcurrency)
func withOutboundEncodeSlot(ctx context.Context, stop <-chan struct{}, fn func() error) error {
if ctx == nil {
ctx = context.Background()
}
select {
case outboundEncodeSlots <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
case <-stop:
return ErrConnClosed
}
defer func() { <-outboundEncodeSlots }()
return fn()
}
func newOutboundTrackedBudget(maxBytes int64) *outboundTrackedBudget {
if maxBytes <= 0 {
maxBytes = defaultOutboundTrackedMaxBytes
}
return &outboundTrackedBudget{
maxBytes: maxBytes,
wake: &outboundBudgetWake{ch: make(chan struct{})},
}
}
func (b *outboundTrackedBudget) reserve(n int) bool {
if n <= 0 {
return true
}
bytes := int64(n)
if b == nil || bytes > b.maxBytes {
return false
}
for {
used := b.used.Load()
if used > b.maxBytes-bytes {
return false
}
if b.used.CompareAndSwap(used, used+bytes) {
return true
}
}
}
func (b *outboundTrackedBudget) release(n int) {
if b == nil || n <= 0 {
return
}
if used := b.used.Add(-int64(n)); used < 0 {
panic("mtprotoedge: outbound tracked byte budget released more than reserved")
}
// A release can make room for multiple independent writes. Broadcast to every waiter in the
// current generation; a capacity-1 token can strand all but one waiter even while bytes are
// available indefinitely. Generations are only allocated on the saturated slow path.
b.wakeMu.Lock()
if b.wake.waiters > 0 {
old := b.wake
b.wake = &outboundBudgetWake{ch: make(chan struct{})}
close(old.ch)
}
b.wakeMu.Unlock()
}
func (b *outboundTrackedBudget) waitReserve(ctx context.Context, stop <-chan struct{}, n int) error {
return b.waitReserveUntil(ctx, stop, n, time.Time{})
}
// waitReserveUntil is the saturated-path variant used by write scratch. The absolute deadline
// is observed from the first capacity wait, not only after encryption when socket I/O begins.
// Its timer is allocated lazily after the CAS fast path fails, preserving the allocation-free
// steady state.
func (b *outboundTrackedBudget) waitReserveUntil(ctx context.Context, stop <-chan struct{}, n int, deadline time.Time) error {
if b == nil || n < 0 || int64(n) > b.maxBytes {
return ErrOutboundTrackedBudget
}
if ctx == nil {
ctx = context.Background()
}
if d, ok := ctx.Deadline(); ok && (deadline.IsZero() || d.Before(deadline)) {
deadline = d
}
var (
deadlineTimer *time.Timer
deadlineC <-chan time.Time
)
defer func() {
if deadlineTimer != nil {
deadlineTimer.Stop()
}
}()
for {
if b.reserve(n) {
return nil
}
if !deadline.IsZero() && !time.Now().Before(deadline) {
return context.DeadlineExceeded
}
// Subscribe under the same lock used by release, then retry once while holding it. This
// closes the release-between-check-and-subscribe window without putting the normal CAS
// reservation path behind a global mutex.
b.wakeMu.Lock()
if b.reserve(n) {
b.wakeMu.Unlock()
return nil
}
generation := b.wake
generation.waiters++
b.wakeMu.Unlock()
if deadlineC == nil && !deadline.IsZero() {
wait := time.Until(deadline)
if wait < 0 {
wait = 0
}
deadlineTimer = time.NewTimer(wait)
deadlineC = deadlineTimer.C
}
var err error
select {
case <-generation.ch:
case <-ctx.Done():
err = ctx.Err()
case <-deadlineC:
err = context.DeadlineExceeded
case <-stop:
err = ErrConnClosed
}
b.wakeMu.Lock()
generation.waiters--
b.wakeMu.Unlock()
if err != nil {
return err
}
}
}
func (b *outboundTrackedBudget) snapshot() int64 {
if b == nil {
return 0
}
return b.used.Load()
}
func newOutboundState(budget *outboundTrackedBudget) *outboundState {
return newOutboundStateWithLimits(budget, maxTrackedServerMsgIDs, maxTrackedServerBytes)
}
func newOutboundStateWithLimits(budget *outboundTrackedBudget, maxMessages, maxBytes int) *outboundState {
return &outboundState{
maxMessages: maxMessages,
maxBytes: maxBytes,
budget: budget,
}
}
func (c *Conn) startOutbound() {
if c.metrics == nil {
c.metrics = NopMetrics{}
}
if c.outboundQueueSize <= 0 {
c.outboundQueueSize = defaultOutboundQueueSize
}
if c.outboundControlQueueSize <= 0 {
c.outboundControlQueueSize = defaultOutboundControlQueueSize
}
c.ensureOutboundTrackedBudget()
c.outbound = make(chan outboundOp, c.outboundQueueSize)
c.outboundControl = make(chan outboundOp, c.outboundControlQueueSize)
c.outboundStop = make(chan struct{})
c.outboundDone = make(chan struct{})
go c.outboundLoop()
}
// Close 停止连接的出站 actor。它不关闭底层 transporttransport 生命周期仍由 serveConn 管理。
func (c *Conn) Close() {
c.beginTerminalShutdown()
c.closeInboundRPCScheduler()
c.waitOutboundShutdown()
}
// beginTerminalShutdown is the non-blocking ownership transition shared by graceful and hard
// close. It closes both producer gates and cancels RPC work before any potentially blocking
// transport.Close call, so a timed-out batch close cannot keep accepting memory/work.
func (c *Conn) beginTerminalShutdown() {
c.terminal.Store(true)
c.signalOutboundStop()
c.beginCloseInboundRPCScheduler()
}
func (c *Conn) waitOutboundShutdown() {
if c.outboundDone != nil {
<-c.outboundDone
}
}
// ForceClose 停止连接并关闭底层 transport。
// 仅用于授权撤销 / destroy_auth_key 这类“必须让对端立即断线”的路径;普通生命周期仍由
// serveConn 统一关闭 transport避免正常 push/索引清理把长连接误伤成硬断。
func (c *Conn) ForceClose() {
c.beginTerminalShutdown()
c.closeTransport()
c.closeInboundRPCScheduler()
c.waitOutboundShutdown()
}
// closeTransport 只关闭物理 transport不等待 outbound actor。写失败路径运行在
// actor 自身 goroutine 中,若在这里调用 Close 会等待 outboundDone 而自锁。
func (c *Conn) closeTransport() {
c.transportClose.Do(func() {
if c.transport != nil {
_ = c.transport.Close()
}
})
}
// failTransport 把不可恢复的写错误提升为连接级 terminal failure。它只负责
// 标记 terminal + 关闭 sockethandleOutboundOp 返回后actor 自己发停止信号并退出,
// serveConn 被 Close 解开 Recv 后负责注销索引。
func (c *Conn) failTransport() {
// Publish both producer gates before Close: a custom/broken transport may block in
// Close, but it must not keep accepting queued bodies or RPC work meanwhile.
c.beginTerminalShutdown()
c.closeTransport()
}
// dropSlowConsumer 把出站队列持续拥塞的连接降级为离线连接。它不能等待 outbound
// actor调用方位于 fan-out 热路径,等待单个慢 socket 会把同一用户的健康设备和
// transactional outbox lane 一起拖住。关闭 transport 会打断可能阻塞的写serveConn
// 随后负责 Unregisterdurable update 由该设备重连后的 getDifference 补偿。
func (c *Conn) dropSlowConsumer() {
c.beginTerminalShutdown()
c.closeTransport()
}
func (c *Conn) signalOutboundStop() {
c.outboundClose.Do(func() {
c.outboundEnqueueMu.Lock()
c.outboundClosing = true
if c.outboundStop != nil {
close(c.outboundStop)
}
c.outboundEnqueueMu.Unlock()
})
}
// Send 加密并发送一条 server 消息。
func (c *Conn) Send(ctx context.Context, t proto.MessageType, msg bin.Encoder) error {
return c.send(ctx, t, msg, false)
}
// SendPriority 加密并优先发送一条 server 控制消息。
func (c *Conn) SendPriority(ctx context.Context, t proto.MessageType, msg bin.Encoder) error {
return c.send(ctx, t, msg, true)
}
// SendBestEffort 只等待消息进入普通 outbound 队列,不等待网络写完成。
// 用于 updates fanout队列拥塞时返回 ErrOutboundQueueFulldurable outbox/getDifference 负责兜底。
func (c *Conn) SendBestEffort(ctx context.Context, t proto.MessageType, msg bin.Encoder, timeout time.Duration) error {
return c.sendBestEffort(ctx, t, msg, nil, timeout)
}
func (c *Conn) SendBestEffortEncoded(ctx context.Context, t proto.MessageType, encoded *encodedOutboundMessage, timeout time.Duration) error {
return c.sendBestEffort(ctx, t, nil, encoded, timeout)
}
func (c *Conn) sendBestEffort(ctx context.Context, t proto.MessageType, msg bin.Encoder, encoded *encodedOutboundMessage, timeout time.Duration) error {
if c.outbound == nil || c.outboundControl == nil {
return ErrConnClosed
}
if c.terminal.Load() {
return ErrConnClosed
}
writeCtx := context.Background()
if ctx != nil {
writeCtx = context.WithoutCancel(ctx)
}
// Encode before registering as an enqueue owner. Encoder is an external interface and may
// block forever; connection shutdown must not wait on it. The subsequent producer gate either
// accepts the completed/tracked body or rejects it and releases the reservation.
op, err := c.newOutboundSendOp(ctx, t, msg, encoded, false)
if err != nil {
// Best-effort durable updates may be dropped under process-wide pressure and
// recovered via getDifference. Do not close this healthy connection merely because
// other slow sockets currently own the shared body budget.
if errors.Is(err, ErrOutboundTrackedBudget) {
c.metrics.OutboundDropped("tracked_global_byte_budget")
}
return err
}
if !c.beginOutboundEnqueue() {
op.releaseReservation(c.outboundTrackedBudget)
return ErrConnClosed
}
defer c.endOutboundEnqueue()
op.ctx = writeCtx
op.enqueuedAt = time.Now()
// 快路径非阻塞入队。fan-out 每 (conn × push) 都走这里,队列有空位时不为
// 本次推送分配任何 timer此前 timeout>0 无条件 WithTimeout稳态白建 timer
select {
case c.outbound <- op:
return nil
case <-c.outboundStop:
op.releaseReservation(c.outboundTrackedBudget)
return ErrConnClosed
default:
}
if timeout == 0 {
op.releaseReservation(c.outboundTrackedBudget)
c.metrics.OutboundDropped("push_queue_full")
return ErrOutboundQueueFull
}
c.metrics.OutboundQueueWait(len(c.outbound), cap(c.outbound))
if ctx == nil {
ctx = context.Background()
}
var timeoutC <-chan time.Time
if timeout > 0 {
timer := time.NewTimer(timeout)
defer timer.Stop()
timeoutC = timer.C
}
select {
case c.outbound <- op:
return nil
case <-timeoutC:
op.releaseReservation(c.outboundTrackedBudget)
c.metrics.OutboundDropped("push_queue_timeout")
return ErrOutboundQueueFull
case <-ctx.Done():
op.releaseReservation(c.outboundTrackedBudget)
return ctx.Err()
case <-c.outboundStop:
op.releaseReservation(c.outboundTrackedBudget)
return ErrConnClosed
}
}
func (c *Conn) send(ctx context.Context, t proto.MessageType, msg bin.Encoder, control bool) error {
return c.sendOutbound(ctx, t, msg, nil, control)
}
func (c *Conn) SendEncoded(ctx context.Context, t proto.MessageType, encoded *encodedOutboundMessage) error {
return c.sendOutbound(ctx, t, nil, encoded, false)
}
func (c *Conn) sendOutbound(ctx context.Context, t proto.MessageType, msg bin.Encoder, encoded *encodedOutboundMessage, control bool) error {
if c.outbound == nil || c.outboundControl == nil {
return ErrConnClosed
}
op, err := c.newOutboundSendOp(ctx, t, msg, encoded, control)
if err != nil {
c.failOutboundBudget(err)
return err
}
if !c.beginOutboundEnqueue() {
op.releaseReservation(c.outboundTrackedBudget)
return ErrConnClosed
}
op.control = control
op.ctx = ctx
op.enqueuedAt = time.Now()
op.done = make(chan outboundResult, 1)
if err := c.enqueueOutboundRegistered(ctx, op); err != nil {
op.releaseReservation(c.outboundTrackedBudget)
c.endOutboundEnqueue()
return err
}
c.endOutboundEnqueue()
select {
case res := <-op.done:
return res.err
case <-ctx.Done():
return ctx.Err()
case <-c.outboundStop:
return ErrConnClosed
}
}
// SendAsync 入队一条 server 消息但不等待发送结果fire-and-forget用于读循环里的控制消息
// ack/pong/new_session_created/bad_msg/future_salts/state_info避免读循环被 outbound 写
// 阻塞而连带卡死。走优先(control)队列保证不被普通 push 拖后;队列满时丢弃并记 metrics——此时
// 连接多已严重拥塞,控制消息丢失由客户端重传 / 读写超时兜底。返回非 nil 仅表示连接已关闭。
func (c *Conn) SendAsync(ctx context.Context, t proto.MessageType, msg bin.Encoder) error {
if c.outbound == nil || c.outboundControl == nil {
return ErrConnClosed
}
if c.terminal.Load() {
return ErrConnClosed
}
op, err := c.newOutboundSendOp(ctx, t, msg, nil, true)
if err != nil {
c.failOutboundBudget(err)
return err
}
if !c.beginOutboundEnqueue() {
op.releaseReservation(c.outboundTrackedBudget)
return ErrConnClosed
}
defer c.endOutboundEnqueue()
op.control = true
op.ctx = ctx
op.enqueuedAt = time.Now()
// done 为 nilfire-and-forgethandleOutboundSend 的 finish 对 nil done 安全跳过。
select {
case c.outboundControl <- op:
return nil
case <-c.outboundStop:
op.releaseReservation(c.outboundTrackedBudget)
return ErrConnClosed
default:
op.releaseReservation(c.outboundTrackedBudget)
c.metrics.OutboundDropped("control_queue_full")
return nil
}
}
// AckServerMessages 接收客户端 msgs_ack释放已确认的 server 出站消息。
func (c *Conn) AckServerMessages(ids []int64) {
if len(ids) == 0 || c.outbound == nil || c.outboundControl == nil || c.terminal.Load() {
return
}
op, err := c.newOutboundVectorOp(outboundAck, ids)
if err != nil {
c.failOutboundBudget(err)
return
}
if !c.beginOutboundEnqueue() {
op.releaseReservation(c.outboundTrackedBudget)
return
}
defer c.endOutboundEnqueue()
select {
case c.outboundControl <- op:
case <-c.outboundStop:
op.releaseReservation(c.outboundTrackedBudget)
default:
op.releaseReservation(c.outboundTrackedBudget)
c.metrics.OutboundDropped("ack_queue_full")
}
}
// OutgoingStateInfo 返回本连接出站消息的状态。返回值中 0 表示无出站侧意见,
// 调用方可继续用入站 connState 兜底。
func (c *Conn) OutgoingStateInfo(ctx context.Context, ids []int64) ([]byte, error) {
if c.outbound == nil {
return nil, ErrConnClosed
}
op, err := c.newOutboundVectorOp(outboundQueryState, ids)
if err != nil {
c.failOutboundBudget(err)
return nil, err
}
op.ctx = ctx
op.done = make(chan outboundResult, 1)
if err := c.enqueueOutbound(ctx, op); err != nil {
op.releaseReservation(c.outboundTrackedBudget)
return nil, err
}
select {
case res := <-op.done:
return res.info, res.err
case <-ctx.Done():
return nil, ctx.Err()
case <-c.outboundStop:
return nil, ErrConnClosed
}
}
// ResendMessages 重发仍在 outgoing queue 中的 server 消息,并返回对应状态。
func (c *Conn) ResendMessages(ctx context.Context, ids []int64) ([]byte, error) {
if c.outbound == nil {
return nil, ErrConnClosed
}
op, err := c.newOutboundVectorOp(outboundResend, ids)
if err != nil {
c.failOutboundBudget(err)
return nil, err
}
op.ctx = ctx
op.done = make(chan outboundResult, 1)
if err := c.enqueueOutbound(ctx, op); err != nil {
op.releaseReservation(c.outboundTrackedBudget)
return nil, err
}
select {
case res := <-op.done:
return res.info, res.err
case <-ctx.Done():
return nil, ctx.Err()
case <-c.outboundStop:
return nil, ErrConnClosed
}
}
// ResendByRequest 在重复 RPC 请求到达时,按原 client msg_id 找到并重发已有 rpc_result。
func (c *Conn) ResendByRequest(ctx context.Context, reqMsgID int64) (bool, error) {
if c.outbound == nil {
return false, ErrConnClosed
}
op := outboundOp{
kind: outboundResendByRequest,
control: true,
ctx: ctx,
reqMsgID: reqMsgID,
done: make(chan outboundResult, 1),
}
if err := c.enqueueOutbound(ctx, op); err != nil {
return false, err
}
select {
case res := <-op.done:
return res.resent, res.err
case <-ctx.Done():
return false, ctx.Err()
case <-c.outboundStop:
return false, ErrConnClosed
}
}
func (c *Conn) enqueueOutbound(ctx context.Context, op outboundOp) error {
if !c.beginOutboundEnqueue() {
return ErrConnClosed
}
defer c.endOutboundEnqueue()
return c.enqueueOutboundRegistered(ctx, op)
}
func (c *Conn) enqueueOutboundRegistered(ctx context.Context, op outboundOp) error {
if ctx == nil {
ctx = context.Background()
}
if c.terminal.Load() {
return ErrConnClosed
}
q := c.outbound
if op.control {
q = c.outboundControl
}
select {
case q <- op:
return nil
case <-ctx.Done():
return ctx.Err()
case <-c.outboundStop:
return ErrConnClosed
default:
}
c.metrics.OutboundQueueWait(len(q), cap(q))
select {
case q <- op:
return nil
case <-ctx.Done():
return ctx.Err()
case <-c.outboundStop:
return ErrConnClosed
}
}
func (c *Conn) beginOutboundEnqueue() bool {
c.outboundEnqueueMu.Lock()
defer c.outboundEnqueueMu.Unlock()
if c.outboundClosing || c.terminal.Load() {
return false
}
c.outboundEnqueueWG.Add(1)
return true
}
func (c *Conn) endOutboundEnqueue() {
c.outboundEnqueueWG.Done()
}
func (c *Conn) outboundLoop() {
state := newOutboundState(c.outboundTrackedBudget)
defer func() {
// pending frames belong exclusively to this actor. Releasing after drain ensures no
// resend path can race the final budget return and no Conn body survives actor exit.
state.releaseAll()
close(c.outboundDone)
}()
for {
if c.terminal.Load() {
c.signalOutboundStop()
c.drainOutbound()
return
}
select {
case op := <-c.outboundControl:
if c.terminal.Load() {
op.releaseReservation(c.outboundTrackedBudget)
op.finish(outboundResult{err: ErrConnClosed})
c.signalOutboundStop()
c.drainOutbound()
return
}
c.handleOutboundOp(state, op)
if c.terminal.Load() {
c.signalOutboundStop()
c.drainOutbound()
return
}
continue
default:
}
select {
case <-c.outboundStop:
c.drainOutbound()
return
case op := <-c.outboundControl:
if c.terminal.Load() {
op.releaseReservation(c.outboundTrackedBudget)
op.finish(outboundResult{err: ErrConnClosed})
c.signalOutboundStop()
c.drainOutbound()
return
}
c.handleOutboundOp(state, op)
case op := <-c.outbound:
if c.terminal.Load() {
op.releaseReservation(c.outboundTrackedBudget)
op.finish(outboundResult{err: ErrConnClosed})
c.signalOutboundStop()
c.drainOutbound()
return
}
c.handleOutboundOp(state, op)
}
if c.terminal.Load() {
c.signalOutboundStop()
c.drainOutbound()
return
}
}
}
func (c *Conn) drainOutbound() {
// signalOutboundStop closes the producer gate before the actor gets here.
// Waiting first guarantees every producer either enqueued an owned op or
// released its reservation, after which this final drain cannot miss a body.
c.outboundEnqueueWG.Wait()
for {
select {
case op := <-c.outboundControl:
op.releaseReservation(c.outboundTrackedBudget)
op.finish(outboundResult{err: ErrConnClosed})
case op := <-c.outbound:
op.releaseReservation(c.outboundTrackedBudget)
op.finish(outboundResult{err: ErrConnClosed})
default:
return
}
}
}
func (c *Conn) handleOutboundOp(state *outboundState, op outboundOp) {
if op.kind != outboundSend {
defer op.releaseReservation(state.budget)
}
switch op.kind {
case outboundSend:
c.handleOutboundSend(state, op)
case outboundAck:
state.ack(op.ids)
case outboundQueryState:
op.finish(outboundResult{info: state.stateInfo(op.ids)})
case outboundResend:
info, err := c.handleOutboundResend(state, op.ctx, op.ids)
op.finish(outboundResult{info: info, err: err})
case outboundResendByRequest:
resent, err := c.handleOutboundResendByRequest(state, op.ctx, op.reqMsgID)
op.finish(outboundResult{resent: resent, err: err})
default:
op.finish(outboundResult{err: fmt.Errorf("unknown outbound op %d", op.kind)})
}
}
func (c *Conn) handleOutboundSend(state *outboundState, op outboundOp) {
frame, err := c.buildFrame(op.ctx, op.msgType, op.msg, op.encoded)
reserved := op.reservedBytes
reservationBudget := op.reservationBudget
if reservationBudget == nil {
reservationBudget = state.budget
}
op.reservedBytes = 0
op.reservationBudget = nil
// A per-connection layer downgrade can allocate a different body. Reserve the
// replacement before dropping the canonical queue reservation so the transient
// two-body peak is also covered by the Server budget.
if err == nil && frame != nil && op.encoded != nil && !sameBacking(frame.body, op.encoded.body) {
if !reservationBudget.reserve(len(frame.body)) {
err = ErrOutboundTrackedBudget
} else {
reservationBudget.release(reserved)
reserved = len(frame.body)
op.encoded = nil
}
}
if err == nil && frame != nil && frameNeedsAck(frame.typeID) {
// The queue reservation is transferred to pending after write. A frame larger
// than the per-Conn resend ceiling is rejected before any bytes hit the wire.
if len(frame.body) > maxTrackedServerBytes {
err = ErrOutboundTrackedBudget
}
}
if errors.Is(err, ErrOutboundTrackedBudget) {
c.metrics.OutboundDropped("tracked_global_byte_budget")
c.failTransport()
}
if err == nil {
err = c.writeFrame(op.ctx, frame)
}
if err == nil && frame != nil && frameNeedsAck(frame.typeID) {
// 写成功后才提交 content seq_no 递增peekSeqNo 已按当前计数算好本帧 seq_no
c.commitContentSeqNo()
frame.reservedBytes = reserved
frame.reservationBudget = reservationBudget
reserved = 0
if dropped := state.addReserved(frame); dropped > 0 {
for i := 0; i < dropped; i++ {
c.metrics.OutboundDropped("tracked_queue_overflow")
}
}
}
reservationBudget.release(reserved)
queueWait := time.Since(op.enqueuedAt)
bytes := 0
typeID := uint32(0)
if frame != nil {
bytes = len(frame.body)
typeID = frame.typeID
}
c.metrics.OutboundSend(typeID, queueWait, bytes, err)
op.finish(outboundResult{err: err})
}
func (c *Conn) handleOutboundResend(state *outboundState, ctx context.Context, ids []int64) ([]byte, error) {
info := make([]byte, len(ids))
resent := 0
for i, id := range ids {
if state.isKnown(id) {
info[i] = msgStateReceived
}
frame, ok := state.pending[id]
if !ok {
continue
}
if err := c.writeFrame(ctx, frame); err != nil {
c.metrics.OutboundResend(resent, err)
return info, err
}
frame.sentAt = time.Now()
frame.sends++
resent++
}
c.metrics.OutboundResend(resent, nil)
return info, nil
}
func (c *Conn) handleOutboundResendByRequest(state *outboundState, ctx context.Context, reqMsgID int64) (bool, error) {
msgID, ok := state.byRequest[reqMsgID]
if !ok {
return false, nil
}
frame, ok := state.pending[msgID]
if !ok {
return false, nil
}
if err := c.writeFrame(ctx, frame); err != nil {
c.metrics.OutboundResend(0, err)
return false, err
}
frame.sentAt = time.Now()
frame.sends++
c.metrics.OutboundResend(1, nil)
return true, nil
}
func (op outboundOp) finish(res outboundResult) {
if op.done == nil {
return
}
select {
case op.done <- res:
default:
}
}
func (op *outboundOp) releaseReservation(budget *outboundTrackedBudget) {
if op == nil || op.reservedBytes <= 0 {
return
}
if op.reservationBudget != nil {
budget = op.reservationBudget
}
bytes := op.reservedBytes
op.reservedBytes = 0
op.encoded = nil
op.msg = nil
op.ids = nil
op.reservationBudget = nil
// Make the queued body/vector unreachable before advertising its bytes to another producer.
budget.release(bytes)
}
func (c *Conn) newOutboundVectorOp(kind outboundOpKind, ids []int64) (outboundOp, error) {
bytes := len(ids) * 8
budget := c.ensureOutboundControlTrackedBudget()
if !budget.reserve(bytes) {
return outboundOp{}, ErrOutboundTrackedBudget
}
return outboundOp{
kind: kind,
control: true,
ids: append([]int64(nil), ids...),
reservedBytes: bytes,
reservationBudget: budget,
}, nil
}
func (c *Conn) newOutboundSendOp(ctx context.Context, t proto.MessageType, msg bin.Encoder, encoded *encodedOutboundMessage, priorityControl bool) (outboundOp, error) {
var budget *outboundTrackedBudget
if encoded == nil {
var bytes int
err := withOutboundEncodeSlot(ctx, c.outboundStop, func() error {
var err error
encoded, err = encodeOutboundMessageWithoutSlot(msg)
if err != nil {
return err
}
if encoded == nil {
return errors.New("nil encoded outbound message")
}
bytes = len(encoded.body)
if bytes > maxOutboundBodyBytes {
return fmt.Errorf("%w: body=%d limit=%d", ErrOutboundMessageTooLarge, bytes, maxOutboundBodyBytes)
}
budget = c.outboundMessageBudget(encoded.typeID, priorityControl)
// Keep the transient encode slot until the completed body has entered the
// retained-byte budget. Otherwise goroutines could successively finish an
// encode, be descheduled before reserve, and accumulate an unbounded number
// of completed-but-untracked bodies despite the encode concurrency gate.
if !budget.reserve(bytes) {
return ErrOutboundTrackedBudget
}
return nil
})
if err != nil {
return outboundOp{}, err
}
return outboundOp{
kind: outboundSend,
msgType: t,
encoded: encoded,
reservedBytes: bytes,
reservationBudget: budget,
}, nil
}
if encoded == nil {
return outboundOp{}, errors.New("nil encoded outbound message")
}
bytes := len(encoded.body)
if bytes > maxOutboundBodyBytes {
return outboundOp{}, fmt.Errorf("%w: body=%d limit=%d", ErrOutboundMessageTooLarge, bytes, maxOutboundBodyBytes)
}
budget = c.outboundMessageBudget(encoded.typeID, priorityControl)
if !budget.reserve(bytes) {
return outboundOp{}, ErrOutboundTrackedBudget
}
return outboundOp{
kind: outboundSend,
msgType: t,
encoded: encoded,
reservedBytes: bytes,
reservationBudget: budget,
}, nil
}
func (c *Conn) outboundMessageBudget(typeID uint32, priorityControl bool) *outboundTrackedBudget {
if priorityControl || encodedControlFrame(typeID) {
return c.ensureOutboundControlTrackedBudget()
}
return c.ensureOutboundTrackedBudget()
}
func (c *Conn) failOutboundBudget(err error) {
if !errors.Is(err, ErrOutboundTrackedBudget) {
return
}
if c.metrics != nil {
c.metrics.OutboundDropped("tracked_global_byte_budget")
}
c.failTransport()
}
func (c *Conn) ensureOutboundTrackedBudget() *outboundTrackedBudget {
c.outboundBudgetOnce.Do(func() {
if c.outboundTrackedBudget == nil {
// Standalone tests/embedders still get a bounded budget. Server-created
// connections receive the shared Server budget before this can run.
c.outboundTrackedBudget = newOutboundTrackedBudget(defaultOutboundTrackedMaxBytes)
}
})
return c.outboundTrackedBudget
}
func (c *Conn) ensureOutboundControlTrackedBudget() *outboundTrackedBudget {
c.outboundControlBudgetOnce.Do(func() {
if c.outboundControlTrackedBudget == nil {
c.outboundControlTrackedBudget = newOutboundTrackedBudget(defaultOutboundControlMaxBytes)
}
})
return c.outboundControlTrackedBudget
}
func (c *Conn) buildFrame(ctx context.Context, t proto.MessageType, msg bin.Encoder, encoded *encodedOutboundMessage) (*outboundFrame, error) {
if encoded == nil {
var err error
encoded, err = encodeOutboundMessage(msg)
if err != nil {
return nil, err
}
}
// 出站统一在此按本连接协商 layer 降级:
// - push fan-out 用 onceEncodedOutbound 把更新编码一次(canonical)再 SendEncoded 给多条
// 连接共享,故必须在此**逐连接**降级,且**绝不改共享 encoded**(downgradedClone 拷贝)。
// - rpc_result 的内层对象已在 encodeRPCResult 按 layer 降级,其 mt.* 外壳在此为顶层直通(no-op)。
// - 控制消息(mt.*)顶层直通。layer>=227 整条零开销。
encoded = c.downgradedCloneContext(ctx, encoded)
content := frameNeedsAck(encoded.typeID)
msgID := c.msgID.New(t)
return &outboundFrame{
msgID: msgID,
seqNo: c.peekSeqNo(content),
typeID: encoded.typeID,
body: encoded.body,
reqMsgID: encoded.reqMsgID,
}, nil
}
// downgradedClone 返回按本连接协商 layer 降级后的消息,**绝不修改入参**——push fan-out
// 多条连接共享同一 encoded逐连接降级必须各自拷贝否则会污染其他连接的字节。
// layer>=227 或 Transcode 直通(mt.* / 无变化)时原样返回入参,零拷贝。降级失败 fail-safe
// 返回 canonical 并计 metrics宁可老客户端对个别长尾对象渲染异常也不让连接/流崩)。
func (c *Conn) downgradedClone(encoded *encodedOutboundMessage) *encodedOutboundMessage {
return c.downgradedCloneContext(context.Background(), encoded)
}
func (c *Conn) downgradedCloneContext(ctx context.Context, encoded *encodedOutboundMessage) *encodedOutboundMessage {
if encoded == nil {
return nil
}
if c.ClientLayer() >= layerwire.CanonicalLayer {
return encoded
}
var down []byte
err := withOutboundEncodeSlot(ctx, c.outboundStop, func() error {
var err error
down, err = layerwire.Transcode(encoded.body, c.ClientLayer())
return err
})
if err != nil {
c.metrics.OutboundDropped("layerwire_downgrade_failed")
return encoded
}
if sameBacking(down, encoded.body) {
return encoded // 直通:未变(mt.*/顶层未知),无需拷贝或重算 typeID
}
out := &encodedOutboundMessage{body: down, typeID: encoded.typeID, reqMsgID: encoded.reqMsgID}
if id, e := (&bin.Buffer{Buf: down}).PeekID(); e == nil {
out.typeID = id
}
return out
}
// sameBacking reports whether a and b share the same backing array (Transcode
// returns its input unchanged for passthrough cases).
func sameBacking(a, b []byte) bool {
return len(a) == len(b) && (len(a) == 0 || &a[0] == &b[0])
}
func encodeOutboundMessage(msg bin.Encoder) (*encodedOutboundMessage, error) {
return encodeOutboundMessageContext(context.Background(), msg)
}
func encodeOutboundMessageContext(ctx context.Context, msg bin.Encoder) (*encodedOutboundMessage, error) {
var encoded *encodedOutboundMessage
err := withOutboundEncodeSlot(ctx, nil, func() error {
var err error
encoded, err = encodeOutboundMessageWithoutSlot(msg)
return err
})
return encoded, err
}
func encodeOutboundMessageWithoutSlot(msg bin.Encoder) (*encodedOutboundMessage, error) {
if msg == nil {
return nil, errors.New("nil outbound message")
}
var body bin.Buffer
if err := msg.Encode(&body); err != nil {
return nil, fmt.Errorf("encode outbound: %w", err)
}
typeID, err := (&bin.Buffer{Buf: body.Raw()}).PeekID()
if err != nil {
return nil, fmt.Errorf("peek outbound type id: %w", err)
}
return &encodedOutboundMessage{
typeID: typeID,
body: body.Raw(),
reqMsgID: outboundRequestMsgID(msg),
}, nil
}
// peekSeqNo 计算本帧的 seq_no但不提交 content 计数递增——递增延到 writeFrame 成功后
// commitContentSeqNo。这样写失败超时/连接关)但连接存活时,下一条 content 帧会复用
// 同一 seq_no 而非留下间隙,避免严格校验的客户端把间隙误判为丢帧。只由 outbound actor 调用。
func (c *Conn) peekSeqNo(content bool) int32 {
seqNo := c.sentContentMessages * 2
if content {
seqNo++
}
return seqNo
}
// commitContentSeqNo 在一条 content 帧成功写出后提交 seq_no 递增。只由 outbound actor 调用。
func (c *Conn) commitContentSeqNo() {
c.sentContentMessages++
}
// deadlineOutboundWriter 是可选的直管写超时接口telesrv-owned compat transport 实现它,
// 让 outbound actor 每帧只做一次 SetWriteDeadline不再为写超时分配 context timer
// gotd transport.Conn 的 Send 本身也只消费 ctx.Deadline不监听 ctx.Done语义等价
type deadlineOutboundWriter interface {
SendDeadline(deadline time.Time, b *bin.Buffer) error
}
func (c *Conn) writeFrame(ctx context.Context, frame *outboundFrame) error {
if ctx == nil {
ctx = context.Background()
}
// One absolute deadline covers the complete write attempt, including global scratch
// admission. Previously writeTimeout only started after scratch was acquired, so one blocked
// writer could make unrelated connections wait for their much longer RPC context deadline.
deadline := c.outboundWriteDeadline(ctx)
pool := c.ensureOutboundScratchPool()
scratch, err := pool.acquireUntil(ctx, c.outboundStop, encryptedOutboundWireLen(len(frame.body)), deadline)
if err != nil {
return fmt.Errorf("reserve outbound write scratch: %w", err)
}
defer pool.release(scratch)
out, err := c.encryptOutboundFrameInto(frame, &scratch.wire)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
// Capacity may become available at the deadline boundary, or encryption itself may consume
// the remaining budget. No socket bytes exist yet, so return a non-terminal timeout instead
// of calling the writer with an already-expired deadline and misclassifying it as a possibly
// partial write.
if err := prewriteDeadlineError(ctx, deadline); err != nil {
return fmt.Errorf("outbound deadline before write: %w", err)
}
writer := c.writer
if writer == nil {
writer = c.transport
}
if dw, ok := writer.(deadlineOutboundWriter); ok {
err = dw.SendDeadline(deadline, out)
} else {
// 回落路径gotd full codec / 测试注入 codec 仍走 ctx deadline。
sendCtx := ctx
cancel := func() {}
if !deadline.IsZero() {
sendCtx, cancel = context.WithDeadline(ctx, deadline)
}
err = writer.Send(sendCtx, out)
cancel()
}
if err != nil {
// 任一 partial write / timeout 都可能破坏 MTProto 帧边界;该 socket
// 不可继续复用。这里只发 terminal 信号,不在 actor 内等待自身退出。
c.failTransport()
return fmt.Errorf("send: %w", err)
}
if frame.sentAt.IsZero() {
frame.sentAt = time.Now()
frame.sends = 1
}
return nil
}
func (c *Conn) outboundWriteDeadline(ctx context.Context) time.Time {
var deadline time.Time
if c.writeTimeout > 0 {
deadline = time.Now().Add(c.writeTimeout)
}
if ctx != nil {
if d, ok := ctx.Deadline(); ok && (deadline.IsZero() || d.Before(deadline)) {
deadline = d
}
}
return deadline
}
func prewriteDeadlineError(ctx context.Context, deadline time.Time) error {
if ctx != nil {
if err := ctx.Err(); err != nil {
return err
}
}
if !deadline.IsZero() && !time.Now().Before(deadline) {
return context.DeadlineExceeded
}
return nil
}
func (c *Conn) ensureOutboundScratchPool() *outboundScratchPool {
c.outboundScratchOnce.Do(func() {
if c.outboundScratchPool == nil {
c.outboundScratchPool = newOutboundScratchPool(defaultOutboundWriteMaxBytes)
}
})
return c.outboundScratchPool
}
func (c *Conn) encryptOutboundFrame(frame *outboundFrame) (*bin.Buffer, error) {
wire := &bin.Buffer{Buf: make([]byte, encryptedOutboundWireLen(len(frame.body)))}
return c.encryptOutboundFrameInto(frame, wire)
}
func encryptedOutboundWireLen(bodyLen int) int {
plainWithoutPadding := encryptedFrameHeaderLen + bodyLen
return 24 + plainWithoutPadding + encryptedPaddingLen(plainWithoutPadding)
}
func (c *Conn) encryptOutboundFrameInto(frame *outboundFrame, wire *bin.Buffer) (*bin.Buffer, error) {
if frame == nil || wire == nil {
return nil, errors.New("nil outbound frame scratch")
}
wireLen := encryptedOutboundWireLen(len(frame.body))
ensureBinBufferLen(wire, wireLen)
plain := wire.Buf[24:]
binary.LittleEndian.PutUint64(plain[0:8], uint64(c.salt))
binary.LittleEndian.PutUint64(plain[8:16], uint64(c.sessionID))
binary.LittleEndian.PutUint64(plain[16:24], uint64(frame.msgID))
binary.LittleEndian.PutUint32(plain[24:28], uint32(frame.seqNo))
binary.LittleEndian.PutUint32(plain[28:32], uint32(len(frame.body)))
copy(plain[encryptedFrameHeaderLen:], frame.body)
paddingOffset := encryptedFrameHeaderLen + len(frame.body)
// padding 随机数走 per-Conn 缓冲读:每帧 12..1024 字节直读 crypto/rand 是一次
// getrandom syscall缓冲后按 ~1KiB 批量取。只由 outbound actor 单 goroutine 访问,
// 随机源本身不变(仍是 cipher 的 CSPRNG只是预读。
if c.outboundRand == nil {
c.outboundRand = bufio.NewReaderSize(c.cipher.Rand(), 1024)
}
if _, err := io.ReadFull(c.outboundRand, plain[paddingOffset:]); err != nil {
return nil, err
}
msgKey := crypto.MessageKey(c.key.Value, plain, crypto.Server)
key, iv := crypto.Keys(c.key.Value, msgKey, crypto.Server)
aesBlock, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
copy(wire.Buf[:len(c.key.ID)], c.key.ID[:])
copy(wire.Buf[len(c.key.ID):len(c.key.ID)+len(msgKey)], msgKey[:])
encryptIGEInPlace(aesBlock, iv[:], plain)
return wire, nil
}
func encryptIGEInPlace(block cipher.Block, iv, buf []byte) {
blockSize := block.BlockSize()
if blockSize != aes.BlockSize || len(iv) != 2*blockSize || len(buf)%blockSize != 0 {
panic("mtprotoedge: invalid in-place IGE dimensions")
}
previousCipher := iv[:blockSize]
var previousPlain [aes.BlockSize]byte
copy(previousPlain[:], iv[blockSize:])
for offset := 0; offset < len(buf); offset += blockSize {
current := buf[offset : offset+blockSize]
var currentPlain [aes.BlockSize]byte
copy(currentPlain[:], current)
for i := range current {
current[i] ^= previousCipher[i]
}
block.Encrypt(current, current)
for i := range current {
current[i] ^= previousPlain[i]
}
previousCipher = current
previousPlain = currentPlain
}
}
func encryptedPaddingLen(l int) int {
return 16 + (16 - (l % 16))
}
func ensureBinBufferLen(b *bin.Buffer, n int) {
if cap(b.Buf) < n {
b.Buf = make([]byte, n)
return
}
b.Buf = b.Buf[:n]
}
func frameNeedsAck(typeID uint32) bool {
switch typeID {
case mt.MsgsAckTypeID,
mt.PongTypeID,
mt.FutureSaltsTypeID,
mt.BadMsgNotificationTypeID,
mt.BadServerSaltTypeID,
mt.MsgsStateInfoTypeID,
mt.MsgsAllInfoTypeID,
mt.MsgDetailedInfoTypeID,
mt.MsgNewDetailedInfoTypeID,
proto.MessageContainerTypeID:
return false
default:
return true
}
}
// encodedControlFrame identifies MTProto service responses independently from content-related
// sequencing. new_session_created and destroy_session_* are content-related (and therefore stay
// in resend tracking until ACK), but their small protocol-critical bodies must not compete with
// RPC results/updates for the general outbound body budget.
func encodedControlFrame(typeID uint32) bool {
switch typeID {
case mt.MsgsAckTypeID,
mt.PongTypeID,
mt.FutureSaltsTypeID,
mt.BadMsgNotificationTypeID,
mt.BadServerSaltTypeID,
mt.MsgsStateInfoTypeID,
mt.MsgsAllInfoTypeID,
mt.MsgDetailedInfoTypeID,
mt.MsgNewDetailedInfoTypeID,
mt.NewSessionCreatedTypeID,
mt.DestroySessionOkTypeID,
mt.DestroySessionNoneTypeID,
mt.RPCAnswerUnknownTypeID,
mt.RPCAnswerDroppedRunningTypeID,
mt.RPCAnswerDroppedTypeID,
destroyAuthKeyOkTypeID,
destroyAuthKeyFailTypeID:
return true
default:
return false
}
}
func outboundRequestMsgID(msg bin.Encoder) int64 {
switch v := msg.(type) {
case *proto.Result:
return v.RequestMessageID
default:
return 0
}
}
// addReserved 接管调用方已经取得的全局 body 预算。pending 的每个元素恰好对应一份
// reservation后续只有 removePending/releaseAll 能归还。
func (s *outboundState) addReserved(frame *outboundFrame) int {
if _, exists := s.pending[frame.msgID]; exists {
panic("mtprotoedge: duplicate outbound msg_id inserted into resend tracking")
}
if s.pending == nil {
s.pending = make(map[int64]*outboundFrame)
}
s.pending[frame.msgID] = frame
s.order = append(s.order, frame.msgID)
s.totalBytes += len(frame.body)
if frame.reqMsgID != 0 {
if s.byRequest == nil {
s.byRequest = make(map[int64]int64)
}
s.byRequest[frame.reqMsgID] = frame.msgID
}
return s.shrinkPending()
}
func (s *outboundState) ack(ids []int64) {
for _, id := range ids {
if !s.removePending(id) {
continue
}
s.markAcked(id)
}
if len(s.order) > s.maxMessages*2 {
s.compactOrder()
}
}
func (s *outboundState) stateInfo(ids []int64) []byte {
info := make([]byte, len(ids))
for i, id := range ids {
if s.isKnown(id) {
info[i] = msgStateReceived
}
}
return info
}
func (s *outboundState) isKnown(id int64) bool {
if _, ok := s.pending[id]; ok {
return true
}
_, ok := s.acked[id]
return ok
}
func (s *outboundState) markAcked(id int64) {
if _, ok := s.acked[id]; ok {
return
}
if s.acked == nil {
s.acked = make(map[int64]struct{})
}
s.acked[id] = struct{}{}
s.ackOrder = append(s.ackOrder, id)
for len(s.ackOrder) > maxTrackedAckedMsgIDs {
oldest := s.ackOrder[0]
s.ackOrder = s.ackOrder[1:]
delete(s.acked, oldest)
}
}
func (s *outboundState) shrinkPending() int {
dropped := 0
for (len(s.pending) > s.maxMessages || s.totalBytes > s.maxBytes) && len(s.order) > 0 {
oldest := s.order[0]
s.order = s.order[1:]
if !s.removePending(oldest) {
continue
}
dropped++
}
return dropped
}
func (s *outboundState) removePending(id int64) bool {
frame, ok := s.pending[id]
if !ok {
return false
}
delete(s.pending, id)
bytes := len(frame.body)
s.totalBytes -= bytes
if frame.reqMsgID != 0 {
if mapped, exists := s.byRequest[frame.reqMsgID]; exists && mapped == id {
delete(s.byRequest, frame.reqMsgID)
}
}
// Clear the body reference before making these bytes available to another connection.
frame.body = nil
frame.releaseReservation(s.budget)
return true
}
func (s *outboundState) releaseAll() {
for _, frame := range s.pending {
frame.body = nil
frame.releaseReservation(s.budget)
}
s.pending = nil
s.order = nil
s.byRequest = nil
s.totalBytes = 0
}
func (f *outboundFrame) releaseReservation(defaultBudget *outboundTrackedBudget) {
if f == nil || f.reservedBytes <= 0 {
return
}
budget := f.reservationBudget
if budget == nil {
budget = defaultBudget
}
bytes := f.reservedBytes
f.reservedBytes = 0
f.reservationBudget = nil
budget.release(bytes)
}
func (s *outboundState) compactOrder() {
filtered := s.order[:0]
for _, id := range s.order {
if _, ok := s.pending[id]; ok {
filtered = append(filtered, id)
}
}
s.order = filtered
}