package mtprotoedge import ( "context" "errors" "sort" "sync" "time" "go.uber.org/zap" "github.com/iamxvbaba/td/bin" "github.com/iamxvbaba/td/proto" "github.com/iamxvbaba/td/tg" "github.com/iamxvbaba/td/tlprofile" ) var ErrSessionAmbiguous = errors.New("session id is shared by multiple auth keys") // ForceClose is intentionally test-only. Production shutdown paths use the // narrower lifecycle primitives so callers cannot bypass ownership rules. func (c *Conn) ForceClose() { c.beginTerminalShutdown() c.closeTransport() c.closeInboundRPCScheduler() c.waitOutboundShutdown() } type rpcReplayStoreForTest struct { mu sync.Mutex results map[rpcExecutionKey]*encodedOutboundMessage } func newRPCReplayStoreForTest() *rpcReplayStoreForTest { return &rpcReplayStoreForTest{results: make(map[rpcExecutionKey]*encodedOutboundMessage)} } func (s *rpcReplayStoreForTest) rpcResult( authKeyID [8]byte, sessionID, reqMsgID int64, ) (*encodedOutboundMessage, bool) { if s == nil { return nil, false } key := rpcExecutionKey{authKeyID: authKeyID, sessionID: sessionID, reqMsgID: reqMsgID} s.mu.Lock() result, ok := s.results[key] s.mu.Unlock() return result, ok } func (s *rpcReplayStoreForTest) put( authKeyID [8]byte, sessionID, reqMsgID int64, encoded *encodedOutboundMessage, ) { key := rpcExecutionKey{authKeyID: authKeyID, sessionID: sessionID, reqMsgID: reqMsgID} s.mu.Lock() s.results[key] = encoded s.mu.Unlock() } func newRPCExecutionLedgerForTest(now func() time.Time, maxPending int) *rpcExecutionLedger { return newRPCExecutionLedger(now, rpcExecutionLedgerCapacity{ maxPending: maxPending, maxPendingPerAuth: maxPending, globalMaxEntries: rpcExecutionMaxEntries, authMaxEntries: rpcExecutionMaxEntries, sessionMaxEntries: rpcExecutionMaxEntries, replayStore: newRPCReplayStoreForTest(), }) } func newRPCExecutionLedgerForServerTest(s *Server, now func() time.Time, maxPending int) *rpcExecutionLedger { if s == nil || s.conns == nil { panic("newRPCExecutionLedgerForServerTest requires a server SessionManager") } return newRPCExecutionLedger(now, rpcExecutionLedgerCapacity{ maxPending: maxPending, maxPendingPerAuth: maxPending, globalMaxEntries: rpcExecutionMaxEntries, authMaxEntries: rpcExecutionMaxEntries, sessionMaxEntries: rpcExecutionMaxEntries, replayStore: s.conns, }) } func (l *rpcExecutionLedger) completeReplayableForTest( authKeyID [8]byte, sessionID, reqMsgID int64, encoded *encodedOutboundMessage, ) { store, ok := l.replayStore.(*rpcReplayStoreForTest) if !ok { panic("completeReplayableForTest requires rpcReplayStoreForTest") } store.put(authKeyID, sessionID, reqMsgID, encoded) l.Complete(authKeyID, sessionID, reqMsgID, encoded, true) } // Conns is a white-box test accessor. Production wires the shared manager // explicitly and does not need a second access path through Server. func (s *Server) Conns() *SessionManager { return s.conns } // Register is a test fixture shortcut for tests that do not exercise the // wire-level required-control barrier. func (m *SessionManager) Register(c *Conn) error { if c == nil { return ErrSessionActivationSuperseded } if c.isActive() { m.mu.RLock() current := m.bySession[connSessionKey(c)] m.mu.RUnlock() if current == c { return nil } return ErrSessionActivationSuperseded } if err := m.BeginActivation(c); err != nil { return err } if err := m.PublishActivation(c); err != nil { m.AbortActivation(c) return err } return nil } func (m *SessionManager) uniqueSessionForTestLocked(sessionID int64) (*Conn, sessionKey, bool, bool) { var ( found *Conn foundKey sessionKey ) for key, c := range m.bySession { if key.sessionID != sessionID { continue } if found != nil { return nil, sessionKey{}, false, true } found, foundKey = c, key } return found, foundKey, found != nil, false } // The session-id-only helpers below preserve focused legacy tests without // carrying an ambiguous global index or API in production. func (m *SessionManager) BindUser(sessionID, userID int64) { m.mu.Lock() defer m.mu.Unlock() c, key, ok, ambiguous := m.uniqueSessionForTestLocked(sessionID) if !ambiguous && ok { m.bindUserLocked(c, key, userID) } } func (m *SessionManager) UserID(sessionID int64) (int64, bool) { m.mu.RLock() c, _, ok, ambiguous := m.uniqueSessionForTestLocked(sessionID) m.mu.RUnlock() if ambiguous || !ok { return 0, false } userID := c.userID.Load() return userID, userID != 0 } func (m *SessionManager) UserIDResolved(sessionID int64) (int64, bool) { m.mu.RLock() c, _, ok, ambiguous := m.uniqueSessionForTestLocked(sessionID) m.mu.RUnlock() if ambiguous || !ok { return 0, false } return c.UserIDResolved() } func (m *SessionManager) UserIDForAuthKey(authKeyID [8]byte, sessionID int64) (int64, bool) { m.mu.RLock() c, ok := m.bySession[sessionKey{authKeyID: authKeyID, sessionID: sessionID}] m.mu.RUnlock() if !ok { return 0, false } userID := c.userID.Load() return userID, userID != 0 } func (m *SessionManager) BindAuthKey(sessionID int64, authKeyID [8]byte) { m.mu.Lock() defer m.mu.Unlock() c, key, ok, ambiguous := m.uniqueSessionForTestLocked(sessionID) if !ambiguous && ok { m.bindAuthKeyLocked(c, key, authKeyID) } } func (m *SessionManager) AuthKeyID(sessionID int64) ([8]byte, bool) { m.mu.RLock() c, _, ok, ambiguous := m.uniqueSessionForTestLocked(sessionID) m.mu.RUnlock() if ambiguous || !ok { return [8]byte{}, false } return c.BusinessAuthKeyID() } func (m *SessionManager) SetReceivesUpdates(sessionID int64, receives bool) { m.mu.Lock() c, key, ok, ambiguous := m.uniqueSessionForTestLocked(sessionID) if ambiguous || !ok { m.mu.Unlock() return } owner, start := m.setReceivesUpdatesLocked(c, key, receives) m.mu.Unlock() if start { go m.runFlush(c, key, owner, 0) } } func (m *SessionManager) SetLayerProfile(sessionID int64, profile tlprofile.Profile) bool { m.mu.RLock() c, _, ok, ambiguous := m.uniqueSessionForTestLocked(sessionID) m.mu.RUnlock() if ambiguous || !ok { return false } return c.SeedLayerProfile(profile) == nil } func (m *SessionManager) PushToSession(ctx context.Context, sessionID int64, t proto.MessageType, msg tg.UpdatesClass) error { m.mu.RLock() c, key, ok, ambiguous := m.uniqueSessionForTestLocked(sessionID) if ambiguous { m.mu.RUnlock() return ErrSessionAmbiguous } if !ok { m.mu.RUnlock() return ErrSessionNotFound } ready := c.receivesUpdates.Load() m.mu.RUnlock() if ready { updates, err := newLayerUpdatesFanoutContext(ctx, msg) if err != nil { return err } encoded, err := updates.prepareForConn(ctx, c) if err != nil { return err } return c.SendEncoded(ctx, t, encoded) } return m.queueOrSendPrepared(ctx, key, t, msg) } func (m *SessionManager) PushToUser(ctx context.Context, userID int64, t proto.MessageType, msg tg.UpdatesClass) (int, error) { return m.PushToUserExceptAuthKeySession(ctx, userID, [8]byte{}, 0, t, msg) } func (m *SessionManager) PushToUserExceptSession(ctx context.Context, userID, excludeSessionID int64, t proto.MessageType, msg tg.UpdatesClass) (int, error) { return m.pushToUser(ctx, userID, nil, excludeSessionID, t, msg) } func (m *SessionManager) PushToUserExceptSessionBestEffort(ctx context.Context, userID, excludeSessionID int64, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) { return m.pushToUserBestEffort(ctx, userID, nil, excludeSessionID, t, msg, timeout) } func (m *SessionManager) Online() int { m.mu.RLock() defer m.mu.RUnlock() return len(m.bySession) } func (m *SessionManager) OnlineChannelIDsAfter(afterChannelID int64, limit int) []int64 { if limit <= 0 { return nil } const maxRecoveryPage = 4096 if limit > maxRecoveryPage { limit = maxRecoveryPage } all := m.OnlineChannelIDsSnapshot() start := sort.Search(len(all), func(i int) bool { return all[i] > afterChannelID }) end := start + limit if end > len(all) { end = len(all) } return all[start:end] } func (m *SessionManager) queueLocked(key sessionKey, t proto.MessageType, msg tg.UpdatesClass) bool { updates, reservation, err := m.preparePendingPush(onceLayerUpdatesFanout(context.Background(), msg)) if err != nil { m.log.Debug("Drop pending push outside byte budget", zap.String("auth_key_id", sessionKeyLog(key.authKeyID)), zap.Int64("session_id", key.sessionID), zap.Error(err), ) return false } defer reservation.release() return m.queuePreparedLocked(key, t, updates, reservation) } func (cs *connState) track(msgID int64, seqNo int32, content bool, state byte) { cs.trackInbound(msgID, seqNo, content, false, state) } func (s *Server) dispatch(ctx context.Context, cs *connState, c *Conn, msgID int64, seqNo int32, b *bin.Buffer, acks *[]int64) error { plan, err := s.preflightInbound(cs, msgID, seqNo, b.Buf) if err != nil { var bad *dispatchBadMsgError if errors.As(err, &bad) && c != nil { return s.sendBadMsg(ctx, c, bad.msgID, bad.seqNo, bad.code) } return err } defer plan.close() plan.commitState(cs) *acks = append(*acks, plan.ackIDs...) return s.executeInboundPlan(ctx, cs, c, plan) } // inboundRPCReservation adapts legacy focused tests to the sole production // reservation state machine: a batch with exactly one entry. type inboundRPCReservation struct { batch *inboundRPCBatchReservation } func (c *Conn) reserveInboundRPC(ctx context.Context, method string, size int) (*inboundRPCReservation, error) { batch, err := c.reserveInboundRPCBatch(ctx, []inboundRPCSpec{{method: method, size: size}}) if err != nil { return nil, err } return &inboundRPCReservation{batch: batch}, nil } func (r *inboundRPCReservation) commit(task inboundRPC) error { if r == nil || r.batch == nil { return ErrConnClosed } return r.batch.commit([]inboundRPC{task}) } func (r *inboundRPCReservation) abort() { if r != nil && r.batch != nil { r.batch.abort() } } func (c *Conn) enqueueInboundRPC(ctx context.Context, task inboundRPC) error { reservation, err := c.reserveInboundRPC(ctx, task.method, task.size) if err != nil { return err } defer reservation.abort() return reservation.commit(task) } func (s *inboundRPCScheduler) budgetSnapshot() (tasks int, bytes int64) { s.budgetMu.Lock() defer s.budgetMu.Unlock() return s.tasks, s.bytes }