feat: sync multilayer td integration
This commit is contained in:
parent
20a310f6ca
commit
766c5db992
491 changed files with 26235 additions and 35340 deletions
|
|
@ -14,43 +14,191 @@ import (
|
|||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotd/td/bin"
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/crypto"
|
||||
"github.com/gotd/td/exchange"
|
||||
"github.com/gotd/td/mt"
|
||||
"github.com/gotd/td/proto"
|
||||
"github.com/gotd/td/proto/codec"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/gotd/td/tmap"
|
||||
"github.com/gotd/td/transport"
|
||||
"github.com/iamxvbaba/td/bin"
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"github.com/iamxvbaba/td/crypto"
|
||||
"github.com/iamxvbaba/td/exchange"
|
||||
"github.com/iamxvbaba/td/mt"
|
||||
"github.com/iamxvbaba/td/proto"
|
||||
"github.com/iamxvbaba/td/proto/codec"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tmap"
|
||||
"github.com/iamxvbaba/td/transport"
|
||||
|
||||
"telesrv/internal/store"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
// RPCHandler 把解密后的 RPC 请求体路由到响应。由 internal/rpc 实现。
|
||||
// legacyRPCHandler 把解密后的 canonical RPC 请求体路由到响应。
|
||||
//
|
||||
// b 是明文 RPC 请求(已剥离 MTProto 外壳);返回的 bin.Encoder 会被包成 rpc_result。
|
||||
// 返回 *tgerr.Error 时连接层将其转为 rpc_error 回发;其他 error 视为连接级故障。
|
||||
type RPCHandler interface {
|
||||
//
|
||||
// 它只保留给本包旧连接状态机的回归测试。生产 API RPC 必须走 LayerRPCHandler,
|
||||
// 使 admission、request/result TypeRef 与 exact profile 不可绕过。
|
||||
type legacyRPCHandler interface {
|
||||
Dispatch(ctx context.Context, authKeyID [8]byte, sessionID int64, b *bin.Buffer) (bin.Encoder, error)
|
||||
// NegotiatedLayer returns the TL layer the session negotiated via
|
||||
// invokeWithLayer and whether one was ever observed. Used to downgrade
|
||||
// outbound objects for clients compiled on an older layer. ok=false means
|
||||
// unknown (cold/evicted) — the caller must keep the connection's last-known
|
||||
// layer rather than overwrite it.
|
||||
// NegotiatedLayer returns the TL layer proven via invokeWithLayer for this
|
||||
// exact (auth_key_id, session_id). It must never infer from API ID, device
|
||||
// metadata, authorization rows, or another session on the same auth key.
|
||||
NegotiatedLayer(authKeyID [8]byte, sessionID int64) (int, bool)
|
||||
}
|
||||
|
||||
// RPCHandlerWithMethod returns the canonical innermost RPC method after the
|
||||
// legacyRPCHandlerWithMethod returns the canonical innermost RPC method after the
|
||||
// router has peeled invokeWithLayer/initConnection/invokeAfter wrappers. Egress
|
||||
// scheduling must use this identity: the outer wrapper is not a useful signal
|
||||
// for prioritizing updates convergence over catalog/media responses.
|
||||
type RPCHandlerWithMethod interface {
|
||||
type legacyRPCHandlerWithMethod interface {
|
||||
DispatchWithMethod(ctx context.Context, authKeyID [8]byte, sessionID int64, b *bin.Buffer) (bin.Encoder, string, error)
|
||||
}
|
||||
|
||||
// LayerRPCHandler is the production API-RPC boundary. Admission is a separate
|
||||
// allocation-bounded phase so the edge can freeze the connection profile,
|
||||
// validate wrapper dependencies and establish exact request identity before
|
||||
// flight/cache/scheduler ownership is acquired.
|
||||
type LayerRPCHandler interface {
|
||||
AdmitLayer(profile tg.LayerProfile, b *bin.Buffer, limits tg.LayerDecodeLimits) (tg.LayerRequest, error)
|
||||
AdmitUnprofiled(b *bin.Buffer, limits tg.LayerDecodeLimits) (tg.LayerRequest, error)
|
||||
DispatchAdmitted(
|
||||
ctx context.Context,
|
||||
authKeyID [8]byte,
|
||||
sessionID int64,
|
||||
msgID int64,
|
||||
admissionSeq uint64,
|
||||
request tg.LayerRequest,
|
||||
) (tg.LayerRPCResult, string, error)
|
||||
}
|
||||
|
||||
// LayerRPCDefaultProfileAdmitter decodes with a recoverable inherited/default
|
||||
// profile. Production handlers should implement it with the same generated
|
||||
// ServerDispatcher and adapter registry used by AdmitLayer. The split keeps old
|
||||
// test doubles source-compatible while allowing invokeWithLayer to correct even
|
||||
// a previously explicit Conn profile.
|
||||
type LayerRPCDefaultProfileAdmitter interface {
|
||||
AdmitDefaultLayer(profile tg.LayerProfile, b *bin.Buffer, limits tg.LayerDecodeLimits) (tg.LayerRequest, error)
|
||||
}
|
||||
|
||||
// LayerRPCSessionProfileResolver may restore an exact profile only when it was
|
||||
// previously proven for this same (auth_key_id, session_id). Auth-key-wide
|
||||
// device metadata is intentionally ineligible: a client upgrade can reuse its
|
||||
// auth key while opening a new session at a newer Layer.
|
||||
type LayerRPCSessionProfileResolver interface {
|
||||
NegotiatedSessionLayer(authKeyID [8]byte, sessionID int64) (int, bool)
|
||||
}
|
||||
|
||||
// LayerRPCOrderedSessionProfileResolver restores both the selected Layer and
|
||||
// the newest invokeWithLayer client msg_id which proved it. The cursor prevents
|
||||
// an old cached request replay on a replacement physical connection from
|
||||
// rolling the logical session back to an older profile.
|
||||
type LayerRPCOrderedSessionProfileResolver interface {
|
||||
NegotiatedSessionLayerEvidence(authKeyID [8]byte, sessionID int64) (layer int, msgID int64, ok bool)
|
||||
}
|
||||
|
||||
// LayerRPCInheritedAuthKeyProfileResolver resolves the best persisted
|
||||
// auth-key-wide client Layer for a new session. Unlike exact same-session
|
||||
// evidence this value is only an inherited default: a later invokeWithLayer may
|
||||
// correct it. Implementations may resolve a temporary raw key through its bound
|
||||
// permanent key, but must not infer from API ID or device strings.
|
||||
type LayerRPCInheritedAuthKeyProfileResolver interface {
|
||||
ResolveInheritedAuthKeyLayer(ctx context.Context, rawAuthKeyID [8]byte) (layer int, found bool, err error)
|
||||
}
|
||||
|
||||
// LayerRPCSessionProfileRegistry atomically freezes and invalidates the exact
|
||||
// same-session profile. Its bounded retention may survive a TCP reconnect and
|
||||
// admit a naked replay while the entry remains live. Expiry or capacity eviction
|
||||
// loses only that proof and therefore fails closed until fresh invokeWithLayer;
|
||||
// it never falls back to auth-key-wide metadata.
|
||||
type LayerRPCSessionProfileRegistry interface {
|
||||
LayerRPCSessionProfileResolver
|
||||
FreezeNegotiatedSessionLayer(authKeyID [8]byte, sessionID int64, layer int) error
|
||||
ForgetNegotiatedSessionLayer(authKeyID [8]byte, sessionID int64)
|
||||
ForgetNegotiatedAuthKey(authKeyID [8]byte)
|
||||
}
|
||||
|
||||
// LayerRPCOrderedSessionProfileRegistry linearizes explicit Layer evidence by
|
||||
// MTProto client msg_id. applied is false for an older or identical duplicate;
|
||||
// the same msg_id with another Layer must return ErrLayerProfileConflict (or an
|
||||
// error wrapping it). Implementations advance the cursor even when Layer is
|
||||
// unchanged.
|
||||
type LayerRPCOrderedSessionProfileRegistry interface {
|
||||
LayerRPCOrderedSessionProfileResolver
|
||||
FreezeNegotiatedSessionLayerAt(authKeyID [8]byte, sessionID int64, layer int, msgID int64) (applied bool, err error)
|
||||
}
|
||||
|
||||
// LayerRPCDurableSessionProfileResolver restores restart-safe raw Layer
|
||||
// evidence. layer may be newer than this binary's generated codec universe;
|
||||
// msgID remains the ordering authority in that case.
|
||||
type LayerRPCDurableSessionProfileResolver interface {
|
||||
ResolveNegotiatedSessionLayerEvidence(
|
||||
ctx context.Context,
|
||||
rawAuthKeyID [8]byte,
|
||||
sessionID int64,
|
||||
) (layer int, msgID int64, found bool, err error)
|
||||
}
|
||||
|
||||
// LayerRPCDurableSessionProfileAdvancer atomically advances exact-session and
|
||||
// auth-key shared-default evidence. publishShared is true only when this exact
|
||||
// observation still owns the durable shared default.
|
||||
type LayerRPCDurableSessionProfileAdvancer interface {
|
||||
AdvanceNegotiatedSessionLayerEvidence(
|
||||
ctx context.Context,
|
||||
rawAuthKeyID [8]byte,
|
||||
sessionID int64,
|
||||
layer int,
|
||||
msgID int64,
|
||||
) (currentLayer int, currentMsgID int64, publishShared bool, err error)
|
||||
}
|
||||
|
||||
type LayerRPCDurableSessionProfileDeleter interface {
|
||||
DeleteNegotiatedSessionLayerEvidence(
|
||||
ctx context.Context,
|
||||
rawAuthKeyID [8]byte,
|
||||
sessionID int64,
|
||||
) (deleted bool, err error)
|
||||
}
|
||||
|
||||
// LayerRPCReplayPreparer reapplies connection-local wrapper state for an
|
||||
// already-executed exact request without consuming its one-shot business
|
||||
// dispatch lease. The returned callback is safe to run only after a successful
|
||||
// cached rpc_result reaches the replacement physical connection.
|
||||
type LayerRPCReplayPreparer interface {
|
||||
PrepareAdmittedReplay(
|
||||
ctx context.Context,
|
||||
authKeyID [8]byte,
|
||||
sessionID int64,
|
||||
msgID int64,
|
||||
admissionSeq uint64,
|
||||
request tg.LayerRequest,
|
||||
) (afterSuccessfulDelivery func() error, err error)
|
||||
}
|
||||
|
||||
// LayerRPCProfileEvidenceContext lets a generated/semantic handler carry the
|
||||
// edge's MTProto msg_id freshness decision through its existing context-based
|
||||
// dispatch API. fresh=false means the request remains fully request-bound: it
|
||||
// may decode, execute and produce an exact result, but it must not publish
|
||||
// mutable Layer/init/readiness/auth-bind state. Production Router implements
|
||||
// this optional decorator; older handlers which have no such shared state stay
|
||||
// source compatible.
|
||||
type LayerRPCProfileEvidenceContext interface {
|
||||
WithLayerRPCProfileEvidenceFresh(ctx context.Context, fresh bool) context.Context
|
||||
}
|
||||
|
||||
// LayerRPCAdmissionProfilePublisher advances the auth-key-wide inherited
|
||||
// default for fresh explicit evidence. admissionSeq is allocated once by the
|
||||
// edge's exact flight owner and globally orders different MTProto sessions;
|
||||
// cached joins/replays never call this hook again.
|
||||
type LayerRPCAdmissionProfilePublisher interface {
|
||||
PublishAdmittedLayerProfileEvidence(
|
||||
ctx context.Context,
|
||||
rawAuthKeyID [8]byte,
|
||||
sessionID int64,
|
||||
msgID int64,
|
||||
admissionSeq uint64,
|
||||
safeFloor uint64,
|
||||
layer int,
|
||||
) error
|
||||
}
|
||||
|
||||
// RPCInitConnectionObserver records wrapper metadata when the edge aliases an
|
||||
// initConnection reissue to an already-running request and therefore correctly
|
||||
// skips a second business Dispatch.
|
||||
|
|
@ -115,8 +263,23 @@ type Options struct {
|
|||
RPCGlobalWorkers int
|
||||
// RPCGlobalMaxTasks 是全进程已预留、排队和执行中的 RPC 条数上限。默认 8192。
|
||||
RPCGlobalMaxTasks int
|
||||
// RPCGlobalMaxBytes 是上述 RPC body 的总字节预算。默认 512 MiB。
|
||||
// RPCGlobalMaxBytes 是上述 RPC 的进程级 memory charge 预算。legacy charge
|
||||
// 等于 copied body;exact charge 是 typed decode 前的保守 materialization
|
||||
// 上界,因此该配置不表示可并发接收 512 MiB wire body。默认 512 MiB。
|
||||
RPCGlobalMaxBytes int64
|
||||
// RPCResultCache* limits bound pending ownership and completed rpc_result
|
||||
// replay state across the full 331-second duplicate horizon. Every owner is
|
||||
// charged simultaneously at global, raw-auth and session scopes. Defaults:
|
||||
// global 262144/64 MiB, auth 32768/32 MiB, session 16384/16 MiB.
|
||||
RPCResultCacheMaxEntries int
|
||||
RPCResultCacheMaxBytes int64
|
||||
RPCResultCacheAuthMaxEntries int
|
||||
RPCResultCacheAuthMaxBytes int64
|
||||
RPCResultCacheSessionMaxEntries int
|
||||
RPCResultCacheSessionMaxBytes int64
|
||||
// RPCResultPendingPerAuth is an additional active-owner bound, independent
|
||||
// from the retained entry limits and RPCGlobalMaxTasks. Default 2048.
|
||||
RPCResultPendingPerAuth int
|
||||
// InboundFrameGlobalMaxBytes 是所有物理连接当前正在处理的 transport wire buffer
|
||||
// 与最大解密 plaintext buffer 的总预算。长度前缀读取后、payload 分配前预留,默认
|
||||
// 512 MiB;非正值使用默认值。
|
||||
|
|
@ -143,8 +306,13 @@ type Options struct {
|
|||
AuthKeys store.AuthKeyStore
|
||||
// ActiveSessions 管理活跃连接。默认新建;传入时可让 RPC 层共享同一注册表。
|
||||
ActiveSessions *SessionManager
|
||||
// RPC 是 typed RPC 路由。nil 时加密 RPC 被丢弃并记录。
|
||||
RPC RPCHandler
|
||||
// legacyRPC is an internal test hook for the pre-exact RPC state machine.
|
||||
// It is deliberately unexported so production callers cannot bypass
|
||||
// generated Layer admission by configuring the canonical-only route.
|
||||
legacyRPC legacyRPCHandler
|
||||
// LayerRPC is the generated exact-profile production path. When configured,
|
||||
// every API request must complete admission before flight/cache scheduling.
|
||||
LayerRPC LayerRPCHandler
|
||||
// Metrics 接收连接层指标。默认 NopMetrics。
|
||||
Metrics Metrics
|
||||
// OnServing is called after the connection intake loops have been installed.
|
||||
|
|
@ -200,6 +368,30 @@ func (o *Options) setDefaults() {
|
|||
if o.RPCGlobalMaxBytes <= 0 {
|
||||
o.RPCGlobalMaxBytes = 512 << 20
|
||||
}
|
||||
if o.RPCResultCacheMaxEntries == 0 {
|
||||
o.RPCResultCacheMaxEntries = rpcResultCacheMaxEntries
|
||||
}
|
||||
if o.RPCResultCacheMaxBytes == 0 {
|
||||
o.RPCResultCacheMaxBytes = rpcResultCacheMaxBytes
|
||||
}
|
||||
if o.RPCResultCacheAuthMaxEntries == 0 {
|
||||
o.RPCResultCacheAuthMaxEntries = rpcResultCacheAuthMaxEntries
|
||||
}
|
||||
if o.RPCResultCacheAuthMaxBytes == 0 {
|
||||
o.RPCResultCacheAuthMaxBytes = rpcResultCacheAuthMaxBytes
|
||||
}
|
||||
if o.RPCResultCacheSessionMaxEntries == 0 {
|
||||
o.RPCResultCacheSessionMaxEntries = rpcResultCacheSessionMaxEntries
|
||||
}
|
||||
if o.RPCResultCacheSessionMaxBytes == 0 {
|
||||
o.RPCResultCacheSessionMaxBytes = rpcResultCacheSessionMaxBytes
|
||||
}
|
||||
if o.RPCResultPendingPerAuth == 0 {
|
||||
o.RPCResultPendingPerAuth = rpcResultFlightMaxPendingPerAuth
|
||||
if o.RPCResultPendingPerAuth > o.RPCGlobalMaxTasks {
|
||||
o.RPCResultPendingPerAuth = o.RPCGlobalMaxTasks
|
||||
}
|
||||
}
|
||||
if o.InboundFrameGlobalMaxBytes <= 0 {
|
||||
o.InboundFrameGlobalMaxBytes = defaultInboundFrameGlobalMaxBytes
|
||||
}
|
||||
|
|
@ -232,6 +424,34 @@ func (o *Options) setDefaults() {
|
|||
}
|
||||
}
|
||||
|
||||
func validateRPCResultCacheOptions(o Options) error {
|
||||
if o.RPCResultCacheMaxEntries <= 0 || o.RPCResultCacheAuthMaxEntries <= 0 || o.RPCResultCacheSessionMaxEntries <= 0 {
|
||||
return fmt.Errorf("rpc_result cache entry limits must be positive")
|
||||
}
|
||||
if o.RPCResultCacheMaxEntries < o.RPCResultCacheAuthMaxEntries ||
|
||||
o.RPCResultCacheAuthMaxEntries < o.RPCResultCacheSessionMaxEntries {
|
||||
return fmt.Errorf("rpc_result cache entry hierarchy must satisfy global >= auth >= session: %d/%d/%d",
|
||||
o.RPCResultCacheMaxEntries, o.RPCResultCacheAuthMaxEntries, o.RPCResultCacheSessionMaxEntries)
|
||||
}
|
||||
if o.RPCResultCacheMaxBytes < int64(maxOutboundBodyBytes) ||
|
||||
o.RPCResultCacheAuthMaxBytes < int64(maxOutboundBodyBytes) ||
|
||||
o.RPCResultCacheSessionMaxBytes < int64(maxOutboundBodyBytes) {
|
||||
return fmt.Errorf("rpc_result cache byte limits must each be at least max outbound body %d: %d/%d/%d",
|
||||
maxOutboundBodyBytes, o.RPCResultCacheMaxBytes, o.RPCResultCacheAuthMaxBytes, o.RPCResultCacheSessionMaxBytes)
|
||||
}
|
||||
if o.RPCResultCacheMaxBytes < o.RPCResultCacheAuthMaxBytes ||
|
||||
o.RPCResultCacheAuthMaxBytes < o.RPCResultCacheSessionMaxBytes {
|
||||
return fmt.Errorf("rpc_result cache byte hierarchy must satisfy global >= auth >= session: %d/%d/%d",
|
||||
o.RPCResultCacheMaxBytes, o.RPCResultCacheAuthMaxBytes, o.RPCResultCacheSessionMaxBytes)
|
||||
}
|
||||
if o.RPCResultPendingPerAuth <= 0 || o.RPCResultPendingPerAuth > o.RPCGlobalMaxTasks ||
|
||||
o.RPCResultPendingPerAuth > o.RPCResultCacheAuthMaxEntries {
|
||||
return fmt.Errorf("rpc_result per-auth pending limit %d must be positive and <= global pending %d and auth entries %d",
|
||||
o.RPCResultPendingPerAuth, o.RPCGlobalMaxTasks, o.RPCResultCacheAuthMaxEntries)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Server 是 MTProto 连接层(mtprotoedge)。
|
||||
//
|
||||
// 职责见 doc.go。它把原始 TCP 字节流转换为「已解密、已识别 session 的 RPC 请求」:
|
||||
|
|
@ -262,7 +482,8 @@ type Server struct {
|
|||
key exchange.PrivateKey
|
||||
authKeys store.AuthKeyStore
|
||||
conns *SessionManager
|
||||
rpc RPCHandler
|
||||
rpc legacyRPCHandler
|
||||
layerRPC LayerRPCHandler
|
||||
metrics Metrics
|
||||
onServing func(net.Addr)
|
||||
cipher crypto.Cipher
|
||||
|
|
@ -281,6 +502,9 @@ type Server struct {
|
|||
// New 创建 Server。
|
||||
func New(opts Options) *Server {
|
||||
opts.setDefaults()
|
||||
if err := validateRPCResultCacheOptions(opts); err != nil {
|
||||
panic(fmt.Sprintf("mtprotoedge: invalid result-cache options: %v", err))
|
||||
}
|
||||
conns := opts.ActiveSessions
|
||||
if conns == nil {
|
||||
conns = NewSessionManager(opts.Logger.Named("sessions"))
|
||||
|
|
@ -309,16 +533,26 @@ func New(opts Options) *Server {
|
|||
key: exchange.PrivateKey{RSA: opts.RSAKey},
|
||||
authKeys: opts.AuthKeys,
|
||||
conns: conns,
|
||||
rpc: opts.RPC,
|
||||
rpc: opts.legacyRPC,
|
||||
layerRPC: opts.LayerRPC,
|
||||
metrics: opts.Metrics,
|
||||
onServing: opts.OnServing,
|
||||
cipher: crypto.NewServerCipher(opts.Rand),
|
||||
clock: opts.Clock,
|
||||
rand: opts.Rand,
|
||||
types: tmap.New(tg.TypesMap(), mt.TypesMap(), proto.TypesMap()),
|
||||
rpcResults: newRPCResultCacheWithFlightLimit(opts.Clock.Now, opts.RPCGlobalMaxTasks),
|
||||
rpcRewrap: newRPCRewrapRegistry(opts.RPCGlobalMaxTasks),
|
||||
admission: newAdmissionController(opts.MaxConnections, opts.MaxConnectionsPerIP, opts.MaxConcurrentHandshakes),
|
||||
rpcResults: newRPCResultCacheWithFairCapacity(opts.Clock.Now, rpcResultCacheCapacity{
|
||||
maxPending: opts.RPCGlobalMaxTasks,
|
||||
maxPendingPerAuth: opts.RPCResultPendingPerAuth,
|
||||
globalMaxBytes: opts.RPCResultCacheMaxBytes,
|
||||
globalMaxEntries: opts.RPCResultCacheMaxEntries,
|
||||
authMaxBytes: opts.RPCResultCacheAuthMaxBytes,
|
||||
authMaxEntries: opts.RPCResultCacheAuthMaxEntries,
|
||||
sessionMaxBytes: opts.RPCResultCacheSessionMaxBytes,
|
||||
sessionMaxEntries: opts.RPCResultCacheSessionMaxEntries,
|
||||
}),
|
||||
rpcRewrap: newRPCRewrapRegistry(opts.RPCGlobalMaxTasks),
|
||||
admission: newAdmissionController(opts.MaxConnections, opts.MaxConnectionsPerIP, opts.MaxConcurrentHandshakes),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue