owpengram-server/internal/mtprotoedge/outbound.go

833 lines
22 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"
"errors"
"fmt"
"io"
"time"
"github.com/gotd/ige"
"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")
)
const (
maxOutboundQueue = 1024
maxTrackedServerMsgIDs = 4096
maxTrackedAckedMsgIDs = 1024
// maxTrackedServerBytes 是 pending已发送待 ack、用于 resend总 body 字节上限。
// 与 maxTrackedServerMsgIDs 并列:客户端从不 ack 时,大响应体按字节滚动丢弃,
// 防 pending 被「4096 条 × 大 body」撑爆。
maxTrackedServerBytes = 64 << 20 // 64 MiB
)
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
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
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
}
func newOutboundState() *outboundState {
return &outboundState{
pending: make(map[int64]*outboundFrame),
byRequest: make(map[int64]int64),
acked: make(map[int64]struct{}),
}
}
func (c *Conn) startOutbound() {
if c.metrics == nil {
c.metrics = NopMetrics{}
}
c.outbound = make(chan outboundOp, maxOutboundQueue)
c.outboundControl = make(chan outboundOp, maxOutboundQueue/4)
c.outboundStop = make(chan struct{})
c.outboundDone = make(chan struct{})
go c.outboundLoop()
}
// Close 停止连接的出站 actor。它不关闭底层 transporttransport 生命周期仍由 serveConn 管理。
func (c *Conn) Close() {
c.closeInboundRPCScheduler()
c.outboundClose.Do(func() {
if c.outboundStop != nil {
close(c.outboundStop)
<-c.outboundDone
}
})
}
// ForceClose 停止连接并关闭底层 transport。
// 仅用于授权撤销 / destroy_auth_key 这类“必须让对端立即断线”的路径;普通生命周期仍由
// serveConn 统一关闭 transport避免正常 push/索引清理把长连接误伤成硬断。
func (c *Conn) ForceClose() {
if c.transport != nil {
_ = c.transport.Close()
}
c.Close()
}
// 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
}
writeCtx := context.Background()
if ctx != nil {
writeCtx = context.WithoutCancel(ctx)
}
op := outboundOp{
kind: outboundSend,
ctx: writeCtx,
msgType: t,
msg: msg,
encoded: encoded,
enqueuedAt: time.Now(),
}
// 快路径非阻塞入队。fan-out 每 (conn × push) 都走这里,队列有空位时不为
// 本次推送分配任何 timer此前 timeout>0 无条件 WithTimeout稳态白建 timer
select {
case c.outbound <- op:
return nil
case <-c.outboundStop:
return ErrConnClosed
default:
}
if timeout == 0 {
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:
c.metrics.OutboundDropped("push_queue_timeout")
return ErrOutboundQueueFull
case <-ctx.Done():
return ctx.Err()
case <-c.outboundStop:
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 := outboundOp{
kind: outboundSend,
control: control,
ctx: ctx,
msgType: t,
msg: msg,
encoded: encoded,
enqueuedAt: time.Now(),
done: make(chan outboundResult, 1),
}
if err := c.enqueueOutbound(ctx, op); err != nil {
return err
}
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
}
op := outboundOp{
kind: outboundSend,
control: true,
ctx: ctx,
msgType: t,
msg: msg,
enqueuedAt: time.Now(),
// done 为 nilfire-and-forgethandleOutboundSend 的 finish 对 nil done 安全跳过。
}
select {
case c.outboundControl <- op:
return nil
case <-c.outboundStop:
return ErrConnClosed
default:
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 {
return
}
copied := append([]int64(nil), ids...)
op := outboundOp{kind: outboundAck, control: true, ids: copied}
select {
case c.outboundControl <- op:
case <-c.outboundStop:
default:
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 := outboundOp{
kind: outboundQueryState,
control: true,
ctx: ctx,
ids: append([]int64(nil), ids...),
done: make(chan outboundResult, 1),
}
if err := c.enqueueOutbound(ctx, op); err != nil {
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 := outboundOp{
kind: outboundResend,
control: true,
ctx: ctx,
ids: append([]int64(nil), ids...),
done: make(chan outboundResult, 1),
}
if err := c.enqueueOutbound(ctx, op); err != nil {
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 ctx == nil {
ctx = context.Background()
}
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) outboundLoop() {
defer close(c.outboundDone)
state := newOutboundState()
for {
select {
case op := <-c.outboundControl:
c.handleOutboundOp(state, op)
continue
default:
}
select {
case <-c.outboundStop:
c.drainOutbound()
return
case op := <-c.outboundControl:
c.handleOutboundOp(state, op)
case op := <-c.outbound:
c.handleOutboundOp(state, op)
}
}
}
func (c *Conn) drainOutbound() {
for {
select {
case op := <-c.outboundControl:
op.finish(outboundResult{err: ErrConnClosed})
case op := <-c.outbound:
op.finish(outboundResult{err: ErrConnClosed})
default:
return
}
}
}
func (c *Conn) handleOutboundOp(state *outboundState, op outboundOp) {
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.msgType, op.msg, op.encoded)
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()
if dropped := state.add(frame); dropped > 0 {
for i := 0; i < dropped; i++ {
c.metrics.OutboundDropped("tracked_queue_overflow")
}
}
}
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 (c *Conn) buildFrame(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.downgradedClone(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 {
if encoded == nil {
return nil
}
if c.ClientLayer() >= layerwire.CanonicalLayer {
return encoded
}
down, err := layerwire.Transcode(encoded.body, c.ClientLayer())
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) {
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()
}
out, err := c.encryptOutboundFrame(frame)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
writer := c.writer
if writer == nil {
writer = c.transport
}
var deadline time.Time
if c.writeTimeout > 0 {
deadline = time.Now().Add(c.writeTimeout)
}
if d, ok := ctx.Deadline(); ok && (deadline.IsZero() || d.Before(deadline)) {
deadline = d
}
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 {
return fmt.Errorf("send: %w", err)
}
if frame.sentAt.IsZero() {
frame.sentAt = time.Now()
frame.sends = 1
}
return nil
}
func (c *Conn) encryptOutboundFrame(frame *outboundFrame) (*bin.Buffer, error) {
plain := &c.outboundPlain
plain.Reset()
plain.PutLong(c.salt)
plain.PutLong(c.sessionID)
plain.PutLong(frame.msgID)
plain.PutInt32(frame.seqNo)
plain.PutInt32(int32(len(frame.body)))
plain.Put(frame.body)
paddingOffset := plain.Len()
paddingLen := encryptedPaddingLen(paddingOffset)
growBinBufferLen(plain, paddingOffset+paddingLen)
// 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.Buf[paddingOffset:]); err != nil {
return nil, err
}
msgKey := crypto.MessageKey(c.key.Value, plain.Raw(), crypto.Server)
key, iv := crypto.Keys(c.key.Value, msgKey, crypto.Server)
aesBlock, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
wireLen := len(c.key.ID) + len(msgKey) + plain.Len()
wire := &c.outboundWire
ensureBinBufferLen(wire, wireLen)
copy(wire.Buf[:len(c.key.ID)], c.key.ID[:])
copy(wire.Buf[len(c.key.ID):len(c.key.ID)+len(msgKey)], msgKey[:])
ige.EncryptBlocks(aesBlock, iv[:], wire.Buf[len(c.key.ID)+len(msgKey):], plain.Raw())
return wire, nil
}
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 growBinBufferLen(b *bin.Buffer, n int) {
if cap(b.Buf) < n {
next := make([]byte, n)
copy(next, b.Buf)
b.Buf = next
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
}
}
func outboundRequestMsgID(msg bin.Encoder) int64 {
switch v := msg.(type) {
case *proto.Result:
return v.RequestMessageID
default:
return 0
}
}
func (s *outboundState) add(frame *outboundFrame) int {
s.pending[frame.msgID] = frame
s.order = append(s.order, frame.msgID)
s.totalBytes += len(frame.body)
if frame.reqMsgID != 0 {
s.byRequest[frame.reqMsgID] = frame.msgID
}
return s.shrinkPending()
}
func (s *outboundState) ack(ids []int64) {
for _, id := range ids {
frame, ok := s.pending[id]
if !ok {
continue
}
delete(s.pending, id)
s.totalBytes -= len(frame.body)
if frame.reqMsgID != 0 {
delete(s.byRequest, frame.reqMsgID)
}
s.markAcked(id)
}
if len(s.order) > maxTrackedServerMsgIDs*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
}
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) > maxTrackedServerMsgIDs || s.totalBytes > maxTrackedServerBytes) && len(s.order) > 0 {
oldest := s.order[0]
s.order = s.order[1:]
frame, ok := s.pending[oldest]
if !ok {
continue
}
delete(s.pending, oldest)
s.totalBytes -= len(frame.body)
if frame.reqMsgID != 0 {
delete(s.byRequest, frame.reqMsgID)
}
dropped++
}
return dropped
}
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
}