227 lines
6 KiB
Go
227 lines
6 KiB
Go
package mtprotoedge
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"runtime/debug"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/iamxvbaba/td/bin"
|
|
"github.com/iamxvbaba/td/proto"
|
|
)
|
|
|
|
const (
|
|
rpcResultGZIPMinBytes = 4 << 10
|
|
rpcResultGZIPMaxInputBytes = (10 << 20) - 1 // gotd client decompression hard limit.
|
|
rpcResultGZIPMinSavedBytes = 1 << 10
|
|
rpcResultGZIPMinSavedDivisor = 12 // Require roughly 8.3% reduction.
|
|
rpcResultGZIPConcurrency = 8
|
|
rpcDeliveryHookConcurrency = 8
|
|
rpcDeliveryHookQueueSize = 1024
|
|
)
|
|
|
|
var rpcResultGZIPSlots = make(chan struct{}, rpcResultGZIPConcurrency)
|
|
|
|
var defaultRPCDeliveryHookExecutor = newRPCDeliveryHookExecutor(rpcDeliveryHookConcurrency, rpcDeliveryHookQueueSize)
|
|
|
|
// ErrRPCDeliveryHookCapacity means an RPC result with a delivery-dependent
|
|
// transition cannot reserve reliable executor capacity. The result must not be
|
|
// written: its connection is fenced and the immutable result stays replayable.
|
|
var ErrRPCDeliveryHookCapacity = errors.New("mtproto rpc delivery hook capacity exhausted")
|
|
|
|
type rpcDeliveryHookTicketState uint32
|
|
|
|
const (
|
|
rpcDeliveryHookTicketReserved rpcDeliveryHookTicketState = iota + 1
|
|
rpcDeliveryHookTicketQueued
|
|
rpcDeliveryHookTicketReleased
|
|
rpcDeliveryHookTicketDone
|
|
)
|
|
|
|
type rpcDeliveryHookTicket struct {
|
|
executor *rpcDeliveryHookExecutor
|
|
state atomic.Uint32
|
|
job rpcDeliveryHookJob
|
|
}
|
|
|
|
type rpcDeliveryHookJob struct {
|
|
next *rpcDeliveryHookJob
|
|
ticket *rpcDeliveryHookTicket
|
|
fn func()
|
|
}
|
|
|
|
// rpcDeliveryHookExecutor has process lifetime. Capacity bounds queued plus
|
|
// running hooks; every physical write reserves a ticket before admission. A
|
|
// successful writer therefore performs only one short O(1) queue append and
|
|
// never waits for capacity or hook work. Failed writes release their ticket,
|
|
// while the shared logical coordinator remains eligible for a later replay.
|
|
type rpcDeliveryHookExecutor struct {
|
|
slots chan struct{}
|
|
|
|
mu sync.Mutex
|
|
cond *sync.Cond
|
|
head *rpcDeliveryHookJob
|
|
tail *rpcDeliveryHookJob
|
|
|
|
panics atomic.Uint64
|
|
}
|
|
|
|
func newRPCDeliveryHookExecutor(workers, capacity int) *rpcDeliveryHookExecutor {
|
|
if workers <= 0 {
|
|
workers = 1
|
|
}
|
|
if capacity < workers {
|
|
capacity = workers
|
|
}
|
|
e := &rpcDeliveryHookExecutor{slots: make(chan struct{}, capacity)}
|
|
e.cond = sync.NewCond(&e.mu)
|
|
for range workers {
|
|
go e.run()
|
|
}
|
|
return e
|
|
}
|
|
|
|
func (e *rpcDeliveryHookExecutor) reserve() (*rpcDeliveryHookTicket, bool) {
|
|
if e == nil {
|
|
return nil, false
|
|
}
|
|
select {
|
|
case e.slots <- struct{}{}:
|
|
ticket := &rpcDeliveryHookTicket{executor: e}
|
|
ticket.state.Store(uint32(rpcDeliveryHookTicketReserved))
|
|
return ticket, true
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
func (t *rpcDeliveryHookTicket) release() {
|
|
if t == nil || t.executor == nil || !t.state.CompareAndSwap(
|
|
uint32(rpcDeliveryHookTicketReserved), uint32(rpcDeliveryHookTicketReleased),
|
|
) {
|
|
return
|
|
}
|
|
<-t.executor.slots
|
|
}
|
|
|
|
func (t *rpcDeliveryHookTicket) submit(fn func()) bool {
|
|
if t == nil || t.executor == nil || fn == nil || !t.state.CompareAndSwap(
|
|
uint32(rpcDeliveryHookTicketReserved), uint32(rpcDeliveryHookTicketQueued),
|
|
) {
|
|
return false
|
|
}
|
|
t.job.ticket = t
|
|
t.job.fn = fn
|
|
t.executor.enqueue(&t.job)
|
|
return true
|
|
}
|
|
|
|
func (e *rpcDeliveryHookExecutor) enqueue(job *rpcDeliveryHookJob) {
|
|
e.mu.Lock()
|
|
if e.tail == nil {
|
|
e.head = job
|
|
} else {
|
|
e.tail.next = job
|
|
}
|
|
e.tail = job
|
|
e.cond.Signal()
|
|
e.mu.Unlock()
|
|
}
|
|
|
|
func (e *rpcDeliveryHookExecutor) run() {
|
|
for {
|
|
e.mu.Lock()
|
|
for e.head == nil {
|
|
e.cond.Wait()
|
|
}
|
|
job := e.head
|
|
e.head = job.next
|
|
if e.head == nil {
|
|
e.tail = nil
|
|
}
|
|
job.next = nil
|
|
e.mu.Unlock()
|
|
e.runOne(job)
|
|
}
|
|
}
|
|
|
|
func (e *rpcDeliveryHookExecutor) runOne(job *rpcDeliveryHookJob) {
|
|
defer func() {
|
|
if recovered := recover(); recovered != nil {
|
|
e.panics.Add(1)
|
|
log.Printf("mtprotoedge: rpc delivery hook panic: %v\n%s", recovered, debug.Stack())
|
|
}
|
|
if job != nil && job.ticket != nil {
|
|
job.ticket.state.Store(uint32(rpcDeliveryHookTicketDone))
|
|
<-e.slots
|
|
}
|
|
}()
|
|
if job != nil && job.fn != nil {
|
|
job.fn()
|
|
}
|
|
}
|
|
|
|
// encodeAdaptiveRPCResultInner returns either the original layer-specific TL
|
|
// object or one complete gzip_packed object. Compression is CPU bounded and is
|
|
// retained only when it materially reduces the non-preemptible transport frame.
|
|
func encodeAdaptiveRPCResultInner(ctx context.Context, stop <-chan struct{}, inner []byte) ([]byte, bool, error) {
|
|
if len(inner) < rpcResultGZIPMinBytes || len(inner) > rpcResultGZIPMaxInputBytes {
|
|
return inner, false, nil
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
select {
|
|
case rpcResultGZIPSlots <- struct{}{}:
|
|
case <-ctx.Done():
|
|
return nil, false, ctx.Err()
|
|
case <-stop:
|
|
return nil, false, ErrConnClosed
|
|
}
|
|
defer func() { <-rpcResultGZIPSlots }()
|
|
|
|
var packed bin.Buffer
|
|
if err := (proto.GZIP{Data: inner}).Encode(&packed); err != nil {
|
|
return nil, false, err
|
|
}
|
|
saved := len(inner) - packed.Len()
|
|
required := max(rpcResultGZIPMinSavedBytes, len(inner)/rpcResultGZIPMinSavedDivisor)
|
|
if saved < required {
|
|
return inner, false, nil
|
|
}
|
|
return packed.Raw(), true, nil
|
|
}
|
|
|
|
// rpcResultPriority is protocol scheduling metadata, not handler business
|
|
// behavior. Difference/state responses converge the update state, while the
|
|
// dialogs+pinned pair converges the initial chat list in both TDesktop and
|
|
// Android. These bootstrap barriers must pass background prefetch regardless of
|
|
// platform or their own encoded size.
|
|
func rpcResultPriority(method string, encoded *encodedOutboundMessage) outboundPriority {
|
|
base := method
|
|
if i := strings.IndexByte(base, '#'); i >= 0 {
|
|
base = base[:i]
|
|
}
|
|
switch base {
|
|
case "updates.getDifference", "updates.getChannelDifference", "updates.getState",
|
|
"messages.getDialogs", "messages.getPinnedDialogs":
|
|
return outboundPriorityCritical
|
|
}
|
|
return classifyOutboundPriority(encoded, false)
|
|
}
|
|
|
|
func (p outboundPriority) String() string {
|
|
switch p {
|
|
case outboundPriorityCritical:
|
|
return "convergence"
|
|
case outboundPriorityBulk:
|
|
return "bulk"
|
|
case outboundPriorityControl:
|
|
return "control"
|
|
default:
|
|
return "normal"
|
|
}
|
|
}
|