internal/mtprotoedge/inbound_rpc.go
Rama main · réplica de lectura del repositorio original.
package mtprotoedge
import (
"container/list"
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"telesrv/internal/transport"
)
// ErrInboundRPCQueueFull means inbound RPC has reached the per-connection or process-level budget.
var ErrInboundRPCQueueFull = errors.New("inbound rpc queue full")
// maxInflightRPCBytes is the memory charge cap for all reserved, queued and
// executing RPCs on a single connection. The legacy path charges the copied body;
// the exact path charges conservatively based on wire size and the inflation of
// generated objects/vectors/interfaces/strings/bytes before typed decode. It is
// not a cap on acceptable wire bytes. The process-level budget applies first as a
// fallback; this additionally isolates a single connection, preventing one client
// from monopolizing global memory.
const maxInflightRPCBytes = 32 << 20 // 32 MiB
// rpcCloseWaitTimeout is the cap for waiting on in-flight RPCs or shared workers
// to exit when a connection/Server closes.
const rpcCloseWaitTimeout = 5 * time.Second
type inboundRPC struct {
ctx context.Context
cancel context.CancelFunc
stopRoot func() bool
stopTimeout func() bool
method string
enqueuedAt time.Time
deadline time.Time
size int
run func(context.Context) error
onTimeout func()
// release drops request-scoped ownership which is independent from the body
// budget (for example a cross-connection in-flight RPC claim). It runs once
// after the request has either published a terminal result or become
// impossible to run; callers should still make the callback idempotent.
release func()
budget *inboundRPCGlobalReservation
ticket *inboundRPCTicket
gate *inboundRPCGate
}
// inboundRPCGate keeps dependency waits out of the shared worker pool. A
// gated task remains within the ordinary queue/task/byte budgets, but workers
// skip it until every prerequisite publishes its terminal execution outcome.
type inboundRPCGate struct {
remaining atomic.Int32
failed atomic.Bool
ready atomic.Bool
wake func()
}
func newInboundRPCGate(prerequisites int, wake func()) *inboundRPCGate {
if prerequisites < 0 {
prerequisites = 0
}
g := &inboundRPCGate{wake: wake}
// One sentinel keeps the gate closed while subscribers are installed; the
// caller resolves it after registration is complete.
g.remaining.Store(int32(prerequisites + 1))
return g
}
func (g *inboundRPCGate) resolve(success bool) {
if g == nil {
return
}
if !success {
g.failed.Store(true)
}
remaining := g.remaining.Add(-1)
if remaining < 0 {
panic("mtproto inbound RPC gate counter underflow")
}
if remaining == 0 && g.ready.CompareAndSwap(false, true) && g.wake != nil {
g.wake()
}
}
func (g *inboundRPCGate) runnable() bool { return g == nil || g.ready.Load() }
func (g *inboundRPCGate) success() bool { return g == nil || (g.ready.Load() && !g.failed.Load()) }
type inboundRPCTicket struct {
onTimeout func()
}
// inboundRPCScheduler is the Server-level shared scheduler. Each Conn in ready
// has at most one valid token; a worker takes only one item from that connection
// at a time, then puts the still-runnable connection back at the tail of the
// queue, so a single hot connection cannot occupy the shared pool for long.
// Workers are created only after the first task arrives; an idle Server does not
// pre-start 256 goroutines.
type inboundRPCScheduler struct {
workers int
maxTasks int
maxBytes int64
// ready is an intrusive scheduler-owned queue rather than a bounded channel. A connection
// has at most one element, and close removes that element in O(1). This prevents closed-Conn
// stale tokens from filling a channel and making every worker block while trying to reschedule.
readyMu sync.Mutex
ready *list.List
readyIndex map[*Conn]*list.Element
readyWake chan struct{}
stopCh chan struct{}
lifecycleMu sync.Mutex
started bool
stopped bool
workersStarted bool
workerWG sync.WaitGroup
budgetMu sync.Mutex
tasks int
bytes int64
}
type inboundRPCGlobalReservation struct {
scheduler *inboundRPCScheduler
size int64
released atomic.Bool
}
// inboundRPCSpec is the bounded admission descriptor between container preflight
// and the RPC scheduler. method is only used for metrics; size is the
// materialization charge. The legacy path equals the request body byte count
// before Copy; the exact path is the conservative memory bound computed before
// typed decode.
type inboundRPCSpec struct {
method string
size int
}
type inboundRPCBatchEntry struct {
global *inboundRPCGlobalReservation
method string
size int
}
// inboundRPCBatchReservation treats the RPCs within one container as a single
// admission unit. reserve reserves the whole batch's global/per-connection count
// and byte budgets at once; commit appends all tasks at once; abort returns the
// whole budget at once. This prevents a container from executing only the first
// half of its batch.
type inboundRPCBatchReservation struct {
conn *Conn
ctx context.Context
entries []inboundRPCBatchEntry
totalSize int64
once sync.Once
}
var errInboundRPCBatchTaskCount = errors.New("inbound rpc batch task count mismatch")
var errInboundRPCBatchSelection = errors.New("inbound rpc batch selection is invalid")
func newInboundRPCScheduler(workers, maxTasks int, maxBytes int64) *inboundRPCScheduler {
if workers <= 0 {
workers = 1
}
if maxTasks <= 0 {
maxTasks = 1
}
if maxBytes <= 0 {
maxBytes = 1
}
return &inboundRPCScheduler{
workers: workers,
maxTasks: maxTasks,
maxBytes: maxBytes,
ready: list.New(),
readyIndex: make(map[*Conn]*list.Element),
readyWake: make(chan struct{}, 1),
stopCh: make(chan struct{}),
}
}
// start lets the shared pool begin consuming. Tasks that entered ready before
// start keep their order, which smooths startup bursts and lets tests verify
// round-robin fairness deterministically.
func (s *inboundRPCScheduler) start() {
s.lifecycleMu.Lock()
if s.stopped {
s.lifecycleMu.Unlock()
return
}
s.started = true
shouldStart := s.readyLen() > 0
s.lifecycleMu.Unlock()
if shouldStart {
s.ensureWorkers()
}
}
func (s *inboundRPCScheduler) ensureWorkers() {
s.lifecycleMu.Lock()
defer s.lifecycleMu.Unlock()
if !s.started || s.stopped || s.workersStarted {
return
}
s.workersStarted = true
s.workerWG.Add(s.workers)
for i := 0; i < s.workers; i++ {
go s.worker()
}
}
func (s *inboundRPCScheduler) stop(timeout time.Duration) {
s.lifecycleMu.Lock()
if !s.stopped {
s.stopped = true
s.budgetMu.Lock()
// Cut off new tasks under the same lock as reserveGlobal; tasks already
// holding a reservation are still returned precisely by the corresponding
// Conn's commit/abort/close path.
close(s.stopCh)
s.budgetMu.Unlock()
}
s.lifecycleMu.Unlock()
done := make(chan struct{})
go func() {
s.workerWG.Wait()
close(done)
}()
if timeout <= 0 {
<-done
return
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-done:
case <-timer.C:
}
}
// reserveGlobalBatch checks and reserves the whole batch's count/bytes within one
// budgetMu critical section. Each returned reservation is still returned
// individually by its task, so a slow RPC does not hold the budget for the whole
// container's completed tasks.
func (s *inboundRPCScheduler) reserveGlobalBatch(sizes []int) ([]*inboundRPCGlobalReservation, string, error) {
reservations := make([]*inboundRPCGlobalReservation, len(sizes))
s.budgetMu.Lock()
defer s.budgetMu.Unlock()
select {
case <-s.stopCh:
return nil, "scheduler_closed", ErrConnClosed
default:
}
if len(sizes) > s.maxTasks-s.tasks {
return nil, "global_task_budget", ErrInboundRPCQueueFull
}
// Subtract item by item from the remaining budget, avoiding overflow of total and s.bytes+total.
remaining := s.maxBytes - s.bytes
var total int64
for i, size := range sizes {
if size < 0 {
size = 0
}
size64 := int64(size)
if size64 > remaining-total {
return nil, "global_byte_budget", ErrInboundRPCQueueFull
}
total += size64
reservations[i] = &inboundRPCGlobalReservation{scheduler: s, size: size64}
}
s.tasks += len(sizes)
s.bytes += total
return reservations, "", nil
}
func (r *inboundRPCGlobalReservation) release() {
if r == nil || r.scheduler == nil {
return
}
if !r.released.CompareAndSwap(false, true) {
return
}
s := r.scheduler
s.budgetMu.Lock()
s.tasks--
s.bytes -= r.size
s.budgetMu.Unlock()
}
// releaseInboundRPCGlobalBatch lets a batch abort/commit-failure return all the
// budget it still holds within one global lock. Each ticket's CAS guarantees
// idempotency with any concurrent release.
func releaseInboundRPCGlobalBatch(reservations []*inboundRPCGlobalReservation) {
var (
scheduler *inboundRPCScheduler
tasks int
bytes int64
)
for _, reservation := range reservations {
if reservation == nil || reservation.scheduler == nil || !reservation.released.CompareAndSwap(false, true) {
continue
}
if scheduler == nil {
scheduler = reservation.scheduler
}
// A batch can only be created by the same scheduler. If a mixed call ever
// appears, it is still returned correctly through a single release without
// debiting the wrong scheduler.
if reservation.scheduler != scheduler {
reservation.scheduler.budgetMu.Lock()
reservation.scheduler.tasks--
reservation.scheduler.bytes -= reservation.size
reservation.scheduler.budgetMu.Unlock()
continue
}
tasks++
bytes += reservation.size
}
if scheduler == nil || tasks == 0 {
return
}
scheduler.budgetMu.Lock()
scheduler.tasks -= tasks
scheduler.bytes -= bytes
scheduler.budgetMu.Unlock()
}
func (s *inboundRPCScheduler) schedule(c *Conn) {
if s == nil || c == nil {
return
}
// rpcReady/rpcClosed and queue membership must be tested/installed while holding rpcMu.
// Otherwise close can remove the old token between the test and enqueue, leaving a new stale
// token behind after the connection is already terminal.
c.rpcMu.Lock()
eligible := c.rpcReady && !c.rpcClosed
added := false
if eligible {
added = s.enqueueReady(c)
}
c.rpcMu.Unlock()
if !added {
return
}
s.signalReady()
s.ensureWorkers()
}
func (s *inboundRPCScheduler) worker() {
defer s.workerWG.Done()
for {
select {
case <-s.stopCh:
return
default:
}
if c := s.popReady(); c != nil {
task, ok, reschedule := c.takeInboundRPC()
if reschedule {
s.schedule(c)
}
if ok {
c.runInboundRPC(task)
}
continue
}
select {
case <-s.readyWake:
case <-s.stopCh:
return
}
}
}
func (s *inboundRPCScheduler) enqueueReady(c *Conn) bool {
select {
case <-s.stopCh:
return false
default:
}
s.readyMu.Lock()
defer s.readyMu.Unlock()
select {
case <-s.stopCh:
return false
default:
}
if _, exists := s.readyIndex[c]; exists {
return false
}
s.readyIndex[c] = s.ready.PushBack(c)
return true
}
func (s *inboundRPCScheduler) popReady() *Conn {
s.readyMu.Lock()
front := s.ready.Front()
if front == nil {
s.readyMu.Unlock()
return nil
}
c, _ := front.Value.(*Conn)
s.ready.Remove(front)
delete(s.readyIndex, c)
hasMore := s.ready.Len() > 0
s.readyMu.Unlock()
if hasMore {
// Wake another worker while this worker begins the task. A capacity-one wake channel is
// sufficient: every pop cascades another wake until the queue is drained.
s.signalReady()
}
return c
}
func (s *inboundRPCScheduler) unschedule(c *Conn) {
if s == nil || c == nil {
return
}
s.readyMu.Lock()
if el := s.readyIndex[c]; el != nil {
s.ready.Remove(el)
delete(s.readyIndex, c)
}
hasMore := s.ready.Len() > 0
s.readyMu.Unlock()
if hasMore {
s.signalReady()
}
}
func (s *inboundRPCScheduler) readyLen() int {
s.readyMu.Lock()
defer s.readyMu.Unlock()
return s.ready.Len()
}
func (s *inboundRPCScheduler) signalReady() {
select {
case s.readyWake <- struct{}{}:
default:
}
}
func (c *Conn) startInboundRPCScheduler(scheduler *inboundRPCScheduler, maxInflight, queueSize int, timeout time.Duration) {
if c.metrics == nil {
c.metrics = NopMetrics{}
}
if maxInflight <= 0 {
maxInflight = 1
}
if queueSize <= 0 {
queueSize = 1
}
rootCtx, cancel := context.WithCancel(context.Background())
c.rpcScheduler = scheduler
c.rpcCancel = cancel
c.rpcTimeout = timeout
c.rpcRootCtx = rootCtx
c.rpcMaxInflight = maxInflight
c.rpcQueueSize = queueSize
// rpcQueue stays nil; only the first successful commit allocates it via append,
// so a silent connection uses zero queue memory.
}
// reserveInboundRPCBatch must be called before any request body Copy within the
// container. The global budget is locked once and the per-connection budget is
// locked once; if either limit is not satisfied the whole batch fails, leaving no
// partial task/byte reservation.
func (c *Conn) reserveInboundRPCBatch(ctx context.Context, specs []inboundRPCSpec) (*inboundRPCBatchReservation, error) {
if ctx == nil {
ctx = context.Background()
}
select {
case <-ctx.Done():
c.dropInboundRPCSpecs(specs, "context_done")
return nil, ctx.Err()
default:
}
if c.isRetired() {
c.dropInboundRPCSpecs(specs, "scheduler_closed")
return nil, ErrConnClosed
}
if c.rpcScheduler == nil {
c.dropInboundRPCSpecs(specs, "scheduler_closed")
return nil, ErrConnClosed
}
normalized := make([]inboundRPCSpec, len(specs))
sizes := make([]int, len(specs))
for i, spec := range specs {
if spec.size < 0 {
spec.size = 0
}
normalized[i] = spec
sizes[i] = spec.size
}
globals, reason, err := c.rpcScheduler.reserveGlobalBatch(sizes)
if err != nil {
c.dropInboundRPCSpecs(normalized, reason)
return nil, err
}
entries := make([]inboundRPCBatchEntry, len(normalized))
var totalSize int64
for i, spec := range normalized {
entries[i] = inboundRPCBatchEntry{
global: globals[i],
method: spec.method,
size: spec.size,
}
totalSize += int64(spec.size)
}
c.rpcMu.Lock()
if err := ctx.Err(); err != nil {
c.rpcMu.Unlock()
releaseInboundRPCGlobalBatch(globals)
c.dropInboundRPCSpecs(normalized, "context_done")
return nil, err
}
if c.rpcClosed || c.isRetired() {
c.rpcMu.Unlock()
releaseInboundRPCGlobalBatch(globals)
c.dropInboundRPCSpecs(normalized, "scheduler_closed")
return nil, ErrConnClosed
}
// Compare by subtraction to avoid overflow when adding len for an adversarial batch.
availableSlots := c.rpcQueueSize - c.rpcReserved - len(c.rpcQueue)
if len(entries) > availableSlots {
c.rpcMu.Unlock()
releaseInboundRPCGlobalBatch(globals)
c.dropInboundRPCSpecs(normalized, "queue_full")
return nil, ErrInboundRPCQueueFull
}
if totalSize > maxInflightRPCBytes-c.inflightRPCBytes.Load() {
c.rpcMu.Unlock()
releaseInboundRPCGlobalBatch(globals)
c.dropInboundRPCSpecs(normalized, "byte_budget")
return nil, ErrInboundRPCQueueFull
}
c.rpcReserved += len(entries)
c.inflightRPCBytes.Add(totalSize)
// Add and close's Wait are ordered by rpcMu: once close sets rpcClosed, no
// further Add can occur. The whole batch reservation needs only one waiter;
// commit/abort also complete only once.
c.rpcReservationWG.Add(1)
c.rpcMu.Unlock()
return &inboundRPCBatchReservation{
conn: c,
ctx: ctx,
entries: entries,
totalSize: totalSize,
}, nil
}
func (c *Conn) dropInboundRPCSpecs(specs []inboundRPCSpec, reason string) {
for _, spec := range specs {
c.metrics.InboundRPCDropped(spec.method, reason)
}
}
// growEntry raises one provisional task's materialization charge without a
// release/reacquire window. Exact gzip admission calls this after the expanded
// size is known but before the generated typed decoder can allocate the request
// graph. A failure leaves every existing reservation unchanged so the caller
// can reject and abort the whole container atomically.
func (r *inboundRPCBatchReservation) growEntry(index, targetSize int) error {
if r == nil || index < 0 || index >= len(r.entries) || targetSize < 0 {
return errInboundRPCBatchSelection
}
entry := &r.entries[index]
if targetSize <= entry.size {
return nil
}
if entry.global == nil || entry.global.scheduler == nil || entry.global.released.Load() {
return ErrConnClosed
}
delta := int64(targetSize) - int64(entry.size)
scheduler := entry.global.scheduler
conn := r.conn
// Match initial admission's lock order: global scheduler budget, then the
// connection queue budget. Release paths never hold rpcMu while acquiring
// budgetMu, so this cannot invert task completion or close.
scheduler.budgetMu.Lock()
defer scheduler.budgetMu.Unlock()
select {
case <-scheduler.stopCh:
return ErrConnClosed
default:
}
if delta > scheduler.maxBytes-scheduler.bytes {
return fmt.Errorf("%w: grow exact admission global byte budget by %d", ErrInboundRPCQueueFull, delta)
}
conn.rpcMu.Lock()
defer conn.rpcMu.Unlock()
if err := r.ctx.Err(); err != nil {
return err
}
if conn.rpcClosed || conn.isRetired() {
return ErrConnClosed
}
if delta > int64(maxInflightRPCBytes)-conn.inflightRPCBytes.Load() {
return fmt.Errorf("%w: grow exact admission connection byte budget by %d", ErrInboundRPCQueueFull, delta)
}
scheduler.bytes += delta
entry.global.size += delta
entry.size = targetSize
r.totalSize += delta
conn.inflightRPCBytes.Add(delta)
return nil
}
// retain keeps a subset of a provisional batch on the original connection and
// global reservations. Exact-layer admission uses this after typed decode has
// classified completed replays, pending joins, admission errors, and fresh
// owners. A retained fresh owner therefore never passes through a release then
// reacquire window where another connection could consume its memory/task
// budget. Entries not retained are returned immediately as one batch.
//
// indices are reservation-entry indices, must be strictly increasing, and have
// a one-to-one correspondence with specs. The original conservative byte
// charge is intentionally retained; a typed request remains reachable from its
// scheduler task until completion, so shrinking it to the wire size would make
// the materialized graph unaccounted.
func (r *inboundRPCBatchReservation) retain(indices []int, specs []inboundRPCSpec) error {
if r == nil || len(indices) != len(specs) || len(indices) > len(r.entries) {
return errInboundRPCBatchSelection
}
retained := make([]inboundRPCBatchEntry, len(indices))
removed := make([]*inboundRPCGlobalReservation, 0, len(r.entries)-len(indices))
var (
previous = -1
retainedSize int64
removedSize int64
)
selected := 0
for output, index := range indices {
if index <= previous || index < 0 || index >= len(r.entries) {
return errInboundRPCBatchSelection
}
previous = index
for selected < index {
entry := r.entries[selected]
removed = append(removed, entry.global)
removedSize += int64(entry.size)
selected++
}
entry := r.entries[index]
// The caller may improve the provisional metric label but may not increase
// or replace its byte charge after materialization has started.
if specs[output].size > entry.size {
return errInboundRPCBatchSelection
}
entry.method = specs[output].method
retained[output] = entry
retainedSize += int64(entry.size)
selected = index + 1
}
for selected < len(r.entries) {
entry := r.entries[selected]
removed = append(removed, entry.global)
removedSize += int64(entry.size)
selected++
}
c := r.conn
c.rpcMu.Lock()
c.rpcReserved -= len(removed)
c.inflightRPCBytes.Add(-removedSize)
r.entries = retained
r.totalSize = retainedSize
c.rpcMu.Unlock()
releaseInboundRPCGlobalBatch(removed)
if len(retained) == 0 {
// Finish the one reservation waiter as part of the same ownership
// transition. abort sees an empty batch and is therefore accounting-only.
r.abort()
}
return nil
}
// commit appends the whole batch of tasks to the queue within one rpcMu critical
// section and immediately publishes the ready token. The protocol barrier must be
// completed before calling commit; delaying token publication cannot stop an
// existing worker from taking new tasks off the same connection's queue, so no
// fake deferred-schedule mode is provided.
func (r *inboundRPCBatchReservation) commit(tasks []inboundRPC) (result error) {
if r == nil {
return ErrConnClosed
}
result = ErrConnClosed
var (
committed bool
reschedule bool
firstQueueLen int
queueCap int
globals []*inboundRPCGlobalReservation
)
r.once.Do(func() {
c := r.conn
// A container reservation may deliberately span protocol-critical barriers
// (session ownership claim and new_session_created's physical write). Queue
// and execution latency starts only when the fully admitted batch becomes
// runnable, not while it is waiting behind those independent barriers.
enqueuedAt := time.Now()
deadline := time.Time{}
if c.rpcTimeout > 0 {
deadline = enqueuedAt.Add(c.rpcTimeout)
}
if ctxDeadline, ok := r.ctx.Deadline(); ok && (deadline.IsZero() || ctxDeadline.Before(deadline)) {
deadline = ctxDeadline
}
globals = make([]*inboundRPCGlobalReservation, len(r.entries))
for i := range r.entries {
globals[i] = r.entries[i].global
}
c.rpcMu.Lock()
c.rpcReserved -= len(r.entries)
if len(tasks) != len(r.entries) {
c.inflightRPCBytes.Add(-r.totalSize)
result = errInboundRPCBatchTaskCount
} else if c.rpcClosed || c.isRetired() {
c.inflightRPCBytes.Add(-r.totalSize)
} else {
prepared := make([]inboundRPC, len(tasks))
// The request deadline starts when admission succeeds, not when a worker
// eventually dequeues the request. This bounds total queue + execution
// latency and lets a queued request emit its explicit timeout on time.
for i := range tasks {
task := tasks[i]
entry := r.entries[i]
if deadline.IsZero() {
task.ctx, task.cancel = context.WithCancel(r.ctx)
} else {
task.ctx, task.cancel = context.WithDeadline(r.ctx, deadline)
}
task.stopRoot = context.AfterFunc(c.rpcRootCtx, task.cancel)
task.method = entry.method
task.enqueuedAt = enqueuedAt
task.deadline = deadline
task.size = entry.size
task.budget = entry.global
ticket := &inboundRPCTicket{}
if task.onTimeout != nil {
onTimeout := task.onTimeout
var timeoutOnce sync.Once
ticket.onTimeout = func() {
timeoutOnce.Do(onTimeout)
}
task.onTimeout = ticket.onTimeout
}
task.ticket = ticket
if task.onTimeout != nil && !task.deadline.IsZero() {
taskCtx := task.ctx
task.stopTimeout = context.AfterFunc(taskCtx, func() {
if errors.Is(taskCtx.Err(), context.DeadlineExceeded) {
c.expireInboundRPCTicket(ticket)
}
})
}
prepared[i] = task
}
firstQueueLen = len(c.rpcQueue) + 1
c.rpcQueue = append(c.rpcQueue, prepared...)
queueCap = c.rpcQueueSize
if c.rpcRunning < c.rpcMaxInflight && !c.rpcReady && c.hasRunnableInboundRPCLocked() {
c.rpcReady = true
reschedule = true
}
committed = true
result = nil
}
c.rpcMu.Unlock()
c.rpcReservationWG.Done()
if !committed {
releaseInboundRPCGlobalBatch(globals)
}
})
if committed {
for i, entry := range r.entries {
r.conn.metrics.InboundRPCQueued(entry.method, firstQueueLen+i, queueCap)
}
if reschedule {
r.conn.rpcScheduler.schedule(r.conn)
}
}
return result
}
func (r *inboundRPCBatchReservation) abort() {
if r == nil {
return
}
r.once.Do(func() {
c := r.conn
c.rpcMu.Lock()
c.rpcReserved -= len(r.entries)
c.inflightRPCBytes.Add(-r.totalSize)
c.rpcMu.Unlock()
c.rpcReservationWG.Done()
globals := make([]*inboundRPCGlobalReservation, len(r.entries))
for i := range r.entries {
globals[i] = r.entries[i].global
}
releaseInboundRPCGlobalBatch(globals)
})
}
func (c *Conn) takeInboundRPC() (task inboundRPC, ok, reschedule bool) {
c.rpcMu.Lock()
defer c.rpcMu.Unlock()
// The ready token is fungible: receiving one token consumes the current
// "scheduled" state. After close, or when another token has already handled it
// first, this is just a harmless stale token.
if !c.rpcReady {
return inboundRPC{}, false, false
}
c.rpcReady = false
if c.rpcClosed || len(c.rpcQueue) == 0 || c.rpcRunning >= c.rpcMaxInflight {
return inboundRPC{}, false, false
}
index := -1
for i := range c.rpcQueue {
if c.rpcQueue[i].gate.runnable() {
index = i
break
}
}
if index < 0 {
return inboundRPC{}, false, false
}
task = c.rpcQueue[index]
if index == 0 {
// The overwhelmingly common FIFO path advances the slice head in O(1).
// Clear the departed element first so its request graph and closures are
// not retained by the shared backing array.
c.rpcQueue[0] = inboundRPC{}
c.rpcQueue = c.rpcQueue[1:]
} else {
copy(c.rpcQueue[index:], c.rpcQueue[index+1:])
last := len(c.rpcQueue) - 1
c.rpcQueue[last] = inboundRPC{}
c.rpcQueue = c.rpcQueue[:last]
}
if len(c.rpcQueue) == 0 {
c.rpcQueue = nil
}
c.rpcRunning++
c.rpcWG.Add(1)
if c.rpcRunning < c.rpcMaxInflight && c.hasRunnableInboundRPCLocked() {
c.rpcReady = true
reschedule = true
}
return task, true, reschedule
}
func (c *Conn) hasRunnableInboundRPCLocked() bool {
if c.rpcReplayRestores > 0 {
return false
}
for i := range c.rpcQueue {
if c.rpcQueue[i].gate.runnable() {
return true
}
}
return false
}
// beginRPCReplayRestore installs a scheduler-level barrier without occupying a
// global RPC worker. The returned idempotent completion function wakes queued
// work only after the last overlapping restore has finished.
func (c *Conn) beginRPCReplayRestore() func() {
if c == nil {
return func() {}
}
active := false
unschedule := false
c.rpcMu.Lock()
if !c.rpcClosed && !c.isRetired() {
c.rpcReplayRestores++
active = true
if c.rpcReady {
c.rpcReady = false
unschedule = true
}
}
c.rpcMu.Unlock()
if unschedule && c.rpcScheduler != nil {
c.rpcScheduler.unschedule(c)
}
var once sync.Once
return func() {
once.Do(func() {
if !active {
return
}
reschedule := false
c.rpcMu.Lock()
c.rpcReplayRestores--
if c.rpcReplayRestores < 0 {
c.rpcMu.Unlock()
panic("mtproto inbound RPC replay restore barrier underflow")
}
if c.rpcReplayRestores == 0 && !c.rpcClosed && c.rpcRunning < c.rpcMaxInflight &&
!c.rpcReady && c.hasRunnableInboundRPCLocked() {
c.rpcReady = true
reschedule = true
}
c.rpcMu.Unlock()
if reschedule && c.rpcScheduler != nil {
c.rpcScheduler.schedule(c)
}
})
}
}
// wakeInboundRPC is safe from a flight completion callback. It does no work
// while the gate's task has not been committed yet; commit performs the same
// runnable scan and publishes the initial scheduler token.
func (c *Conn) wakeInboundRPC() {
if c == nil || c.rpcScheduler == nil {
return
}
reschedule := false
c.rpcMu.Lock()
if !c.rpcClosed && c.rpcRunning < c.rpcMaxInflight && !c.rpcReady && c.hasRunnableInboundRPCLocked() {
c.rpcReady = true
reschedule = true
}
c.rpcMu.Unlock()
if reschedule {
c.rpcScheduler.schedule(c)
}
}
func (c *Conn) runInboundRPC(task inboundRPC) {
defer c.finishInboundRPC(task)
now := time.Now()
ctxErr := task.ctx.Err()
if (!task.deadline.IsZero() && !now.Before(task.deadline)) || errors.Is(ctxErr, context.DeadlineExceeded) {
c.metrics.InboundRPCDropped(task.method, "queue_timeout")
if task.onTimeout != nil {
task.onTimeout()
}
return
}
if ctxErr != nil {
c.metrics.InboundRPCDropped(task.method, "context_done")
return
}
c.metrics.InboundRPCStarted(task.method, now.Sub(task.enqueuedAt))
ctx := task.ctx
// Inject the connection's peer IP as neutral transport metadata, written to
// authorizations.ip when binding a device authorization. The edge only produces
// transport facts; they are consumed by the RPC layer's transport.ClientIPFrom
// to avoid a reverse dependency.
if ip := c.clientIP(); ip != "" {
ctx = transport.WithClientIP(ctx, ip)
}
if task.run != nil {
_ = task.run(ctx)
}
}
func (c *Conn) finishInboundRPC(task inboundRPC) {
stopInboundRPCTask(task)
var reschedule bool
c.rpcMu.Lock()
c.rpcRunning--
c.inflightRPCBytes.Add(-int64(task.size))
if !c.rpcClosed && c.rpcRunning < c.rpcMaxInflight && !c.rpcReady && c.hasRunnableInboundRPCLocked() {
c.rpcReady = true
reschedule = true
}
c.rpcMu.Unlock()
reservation := task.budget
release := task.release
// The scheduler budget may be reused immediately after release. Clear request-owned
// closures/context references first so slow metrics/rescheduling cannot overlap the old body
// with a newly admitted body under the same byte accounting.
task = inboundRPC{}
reservation.release()
if release != nil {
release()
}
c.rpcWG.Done()
if reschedule {
c.rpcScheduler.schedule(c)
}
}
// expireInboundRPCTicket removes a request that is still queued and returns its
// memory/task reservations immediately. If the worker won the dequeue race, the
// callback does nothing: the running handler owns the only terminal response and
// its deadline is represented solely by context cancellation.
func (c *Conn) expireInboundRPCTicket(ticket *inboundRPCTicket) {
if ticket == nil {
return
}
var (
task inboundRPC
found bool
unschedule bool
)
c.rpcMu.Lock()
for i := range c.rpcQueue {
if c.rpcQueue[i].ticket != ticket {
continue
}
task = c.rpcQueue[i]
copy(c.rpcQueue[i:], c.rpcQueue[i+1:])
last := len(c.rpcQueue) - 1
c.rpcQueue[last] = inboundRPC{}
c.rpcQueue = c.rpcQueue[:last]
if len(c.rpcQueue) == 0 {
c.rpcQueue = nil
if c.rpcReady {
c.rpcReady = false
unschedule = true
}
}
c.inflightRPCBytes.Add(-int64(task.size))
found = true
break
}
c.rpcMu.Unlock()
if unschedule {
c.rpcScheduler.unschedule(c)
}
if found {
method := task.method
reservation := task.budget
release := task.release
stopInboundRPCTask(task)
// Drop the run/context closures before returning the byte reservation. Otherwise an
// onTimeout callback that blocks or performs a slow write can keep the copied request body
// reachable after the global scheduler has advertised those bytes as available again.
task = inboundRPC{}
reservation.release()
c.metrics.InboundRPCDropped(method, "queue_timeout")
if ticket.onTimeout != nil {
ticket.onTimeout()
}
if release != nil {
release()
}
return
}
}
// stopInboundRPCTask disarms queue-expiration cleanup before canceling the
// context. Once a worker dequeues the task, the deadline only cancels the
// handler; it never races the handler with an early RPC_TIMEOUT response.
func stopInboundRPCTask(task inboundRPC) {
if task.stopTimeout != nil {
task.stopTimeout()
}
if task.stopRoot != nil {
task.stopRoot()
}
if task.cancel != nil {
task.cancel()
}
}
func (c *Conn) closeInboundRPCScheduler() {
c.beginCloseInboundRPCScheduler()
if c.rpcScheduler == nil {
return
}
c.waitInboundShutdown(rpcCloseWaitTimeout)
}
// beginCloseInboundRPCScheduler publishes closure, cancels running work and releases queued
// requests without waiting for handlers. Shutdown publishes this phase before transport.Close so
// a pathological/blocking transport implementation cannot leave the RPC admission gate open.
func (c *Conn) beginCloseInboundRPCScheduler() {
if c.rpcScheduler == nil {
return
}
c.rpcClose.Do(func() {
c.rpcMu.Lock()
c.rpcClosed = true
c.rpcReady = false
queued := c.rpcQueue
c.rpcQueue = nil
for i := range queued {
c.inflightRPCBytes.Add(-int64(queued[i].size))
}
c.rpcMu.Unlock()
// Remove the scheduler-owned token after rpcClosed/rpcReady become visible. schedule()
// takes rpcMu while installing a token, so either it finishes first and is removed here,
// or it observes the closed state and cannot enqueue a new stale token afterward.
c.rpcScheduler.unschedule(c)
if c.rpcCancel != nil {
c.rpcCancel()
}
for i := range queued {
task := queued[i]
queued[i] = inboundRPC{}
method := task.method
reservation := task.budget
release := task.release
stopInboundRPCTask(task)
task = inboundRPC{}
reservation.release()
if release != nil {
release()
}
c.metrics.InboundRPCDropped(method, "connection_closed")
}
})
}
// waitInboundShutdown waits for the pre-Copy reservation to complete its
// commit/abort, and for RPCs already dequeued on this connection to finish, both
// sharing one timeout. After the timeout the reservation/shared worker converge
// on their own when the underlying call finally returns; the connection root
// context has already been canceled.
func (c *Conn) waitInboundShutdown(timeout time.Duration) bool {
done := make(chan struct{})
go func() {
c.rpcReservationWG.Wait()
c.rpcWG.Wait()
close(done)
}()
if timeout <= 0 {
return false
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-done:
return true
case <-timer.C:
return false
}
}