merged from gramsrv upstream

This commit is contained in:
onysd 2026-09-01 12:06:31 +03:00
parent 79c64ee916
commit 21a0856587
651 changed files with 54774 additions and 4590 deletions

View file

@ -61,6 +61,10 @@ const (
maxChannelSubscriptionsPerSession = 10
defaultChannelSubscriptionTTL = 75 * time.Second
maxChannelSubscriptionTTL = 2 * time.Minute
// A claim is normally released by its rpc_result delivery callback or the
// pending-update flush it starts. This lease only recovers the exceptional
// path where result encoding is replaced before the callback can be attached.
updatesActivationClaimTTL = time.Minute
)
// forceCloseBatchTimeout is one deadline for a whole revoke/replace/eviction batch. Conn.Close
@ -201,7 +205,9 @@ type SessionManager struct {
// 去 Google 化设备)下仍能收到来电、消息等实时推送。登记后在 pushToUserWithSender
// 中被视为【永久就绪】,绕过 receivesUpdates 门槛直接投递,而不是排队等一个永远
// 不会到来的 getState。See memory: call-inactive-account-network-pause。
pushSessions map[[8]byte]map[int64]struct{}
pushSessions map[[8]byte]map[int64]struct{}
updatesActivationSeq uint64
bootstrapProbeSeq uint64
lifecycle SessionLifecycleObserver
log *zap.Logger
@ -821,6 +827,8 @@ func (m *SessionManager) bindUserLocked(c *Conn, key sessionKey, userID int64) {
if old != userID {
m.clearSessionChannelIndexesLocked(c, key)
c.membershipsSynced.Store(false)
m.clearUpdatesActivationLocked(c)
m.clearBootstrapProbeLocked(c)
// 身份变化即丢弃暂存推送:它们属于前一个账号,flush 给新账号是跨账号泄露。
// 同时取消进行中的排空(runFlush 还另有 owner 校验做批内兜底)。
m.deletePendingLocked(key)
@ -833,6 +841,8 @@ func (m *SessionManager) bindUserLocked(c *Conn, key sessionKey, userID int64) {
} else {
m.clearSessionChannelIndexesLocked(c, key)
c.membershipsSynced.Store(false)
m.clearUpdatesActivationLocked(c)
m.clearBootstrapProbeLocked(c)
m.deletePendingLocked(key)
delete(m.flushing, key)
}
@ -898,6 +908,8 @@ func (m *SessionManager) bindAuthKeyLocked(c *Conn, key sessionKey, authKeyID [8
}
m.clearSessionChannelIndexesLocked(c, key)
c.membershipsSynced.Store(false)
m.clearUpdatesActivationLocked(c)
m.clearBootstrapProbeLocked(c)
m.deletePendingLocked(key)
delete(m.flushing, key)
c.userID.Store(0)
@ -1166,6 +1178,8 @@ func (m *SessionManager) UnbindAuthKey(authKeyID [8]byte) int {
}
m.clearSessionChannelIndexesLocked(c, key)
c.membershipsSynced.Store(false)
m.clearUpdatesActivationLocked(c)
m.clearBootstrapProbeLocked(c)
// 授权解除后暂存推送属于已登出的账号,不能等下一个登录者置位时 flush 出去。
m.deletePendingLocked(key)
delete(m.flushing, key)
@ -1185,6 +1199,7 @@ func (m *SessionManager) setReceivesUpdatesLocked(c *Conn, key sessionKey, recei
c.receivesUpdates.Store(false)
m.clearSessionChannelIndexesLocked(c, key)
c.membershipsSynced.Store(false)
m.clearUpdatesActivationLocked(c)
// 取消进行中的排空激活:runFlush 在置位前会复查该标志,标志已删则放弃置位,
// 避免把刚置 false 的开关翻回 true。
delete(m.flushing, key)
@ -1198,11 +1213,15 @@ func (m *SessionManager) setReceivesUpdatesLocked(c *Conn, key sessionKey, recei
c.receivesUpdates.Store(false)
m.clearSessionChannelIndexesLocked(c, key)
c.membershipsSynced.Store(false)
m.clearUpdatesActivationLocked(c)
delete(m.flushing, key)
return 0, false
}
if c.receivesUpdates.Load() || m.flushing[key] {
// 已就绪,或已有排空协程在跑(完成时会自行取走新增暂存并置位)。
if c.receivesUpdates.Load() {
m.clearUpdatesActivationLocked(c)
}
return 0, false
}
if len(m.pending[key]) == 0 {
@ -1232,6 +1251,7 @@ func (m *SessionManager) runFlush(c *Conn, key sessionKey, owner int64, attempt
// 排空期间发生登出/换号:剩余暂存属于旧账号,丢弃且不得发给新账号。
m.deletePendingLocked(key)
delete(m.flushing, key)
m.clearUpdatesActivationLocked(c)
m.mu.Unlock()
return
}
@ -1239,6 +1259,7 @@ func (m *SessionManager) runFlush(c *Conn, key sessionKey, owner int64, attempt
if len(batch) == 0 {
c.receivesUpdates.Store(true)
delete(m.flushing, key)
m.clearUpdatesActivationLocked(c)
m.mu.Unlock()
return
}
@ -1250,6 +1271,7 @@ func (m *SessionManager) runFlush(c *Conn, key sessionKey, owner int64, attempt
m.mu.Lock()
m.deletePendingLocked(key)
delete(m.flushing, key)
m.clearUpdatesActivationLocked(c)
m.mu.Unlock()
releaseQueuedPushes(batch[i:])
return
@ -1296,6 +1318,7 @@ func (m *SessionManager) runFlush(c *Conn, key sessionKey, owner int64, attempt
if c.userID.Load() != owner {
m.deletePendingLocked(key)
delete(m.flushing, key)
m.clearUpdatesActivationLocked(c)
}
m.mu.Unlock()
releaseQueuedPushes(batch[i:])
@ -1316,6 +1339,7 @@ func (m *SessionManager) runFlush(c *Conn, key sessionKey, owner int64, attempt
c.receivesUpdates.Store(true)
m.deletePendingLocked(key)
delete(m.flushing, key)
m.clearUpdatesActivationLocked(c)
m.mu.Unlock()
m.log.Debug("Flush gave up after retries; activated with getDifference fallback",
zap.String("auth_key_id", sessionKeyLog(key.authKeyID)),
@ -1356,6 +1380,119 @@ func (m *SessionManager) ReceivesUpdatesForAuthKey(authKeyID [8]byte, sessionID
return hasProfile && c.receivesUpdates.Load() && c.membershipsSynced.Load()
}
// BeginSessionUpdatesActivation claims the readiness transition for the
// current physical connection. Ordinary startup RPCs race here before they
// register delivery hooks, so at most one of them can enqueue the expensive
// channel-membership synchronization. Cursor commits remain request-owned.
func (m *SessionManager) BeginSessionUpdatesActivation(authKeyID [8]byte, sessionID int64) (uint64, bool) {
if m == nil {
return 0, false
}
key := sessionKey{authKeyID: authKeyID, sessionID: sessionID}
m.mu.Lock()
defer m.mu.Unlock()
c := m.bySession[key]
if c == nil || c.isRetired() {
return 0, false
}
if _, hasProfile := c.LayerProfile(); hasProfile && c.receivesUpdates.Load() && c.membershipsSynced.Load() {
return 0, false
}
now := time.Now()
if c.now != nil {
now = c.now()
}
if c.updatesActivationToken != 0 {
// A pending FIFO flush owns the activation until it reaches a terminal
// outcome. Never lease-steal while that ordered delivery is in progress.
if m.flushing[key] || now.Sub(c.updatesActivationAt) < updatesActivationClaimTTL {
return 0, false
}
}
m.updatesActivationSeq++
if m.updatesActivationSeq == 0 {
m.updatesActivationSeq++
}
c.updatesActivationToken = m.updatesActivationSeq
c.updatesActivationAt = now
return c.updatesActivationToken, true
}
// EndSessionUpdatesActivation releases only the token owned by the caller and
// only on the same current physical Conn. If SetReceivesUpdates started an
// ordered pending flush, that flush retains and releases the claim itself.
func (m *SessionManager) EndSessionUpdatesActivation(authKeyID [8]byte, sessionID int64, token uint64) {
if m == nil || token == 0 {
return
}
key := sessionKey{authKeyID: authKeyID, sessionID: sessionID}
m.mu.Lock()
defer m.mu.Unlock()
c := m.bySession[key]
if c == nil || c.updatesActivationToken != token || m.flushing[key] {
return
}
m.clearUpdatesActivationLocked(c)
}
// BeginSessionBootstrapProbe claims the first durable bootstrap-job lookup for
// the current physical connection generation. Unlike updates activation, this
// is completed only by a delivered getState/getDifference baseline.
func (m *SessionManager) BeginSessionBootstrapProbe(authKeyID [8]byte, sessionID int64) (uint64, bool) {
if m == nil {
return 0, false
}
key := sessionKey{authKeyID: authKeyID, sessionID: sessionID}
m.mu.Lock()
defer m.mu.Unlock()
c := m.bySession[key]
if c == nil || c.isRetired() || c.bootstrapProbed || c.bootstrapProbeToken != 0 {
return 0, false
}
m.bootstrapProbeSeq++
if m.bootstrapProbeSeq == 0 {
m.bootstrapProbeSeq++
}
c.bootstrapProbeToken = m.bootstrapProbeSeq
return c.bootstrapProbeToken, true
}
// EndSessionBootstrapProbe completes or releases only the token on the same
// current Conn. A delayed callback from a replaced connection cannot mutate the
// replacement's one-shot state.
func (m *SessionManager) EndSessionBootstrapProbe(authKeyID [8]byte, sessionID int64, token uint64, success bool) {
if m == nil || token == 0 {
return
}
key := sessionKey{authKeyID: authKeyID, sessionID: sessionID}
m.mu.Lock()
defer m.mu.Unlock()
c := m.bySession[key]
if c == nil || c.bootstrapProbeToken != token {
return
}
c.bootstrapProbeToken = 0
if success {
c.bootstrapProbed = true
}
}
func (m *SessionManager) clearUpdatesActivationLocked(c *Conn) {
if c == nil {
return
}
c.updatesActivationToken = 0
c.updatesActivationAt = time.Time{}
}
func (m *SessionManager) clearBootstrapProbeLocked(c *Conn) {
if c == nil {
return
}
c.bootstrapProbeToken = 0
c.bootstrapProbed = false
}
// SetReceivesUpdatesForAuthKey 标记指定 raw auth_key_id + session_id 是否接收主动 updates。
func (m *SessionManager) SetReceivesUpdatesForAuthKey(authKeyID [8]byte, sessionID int64, receives bool) {
m.mu.Lock()
@ -1470,11 +1607,32 @@ func (m *SessionManager) PushToUserAuthKeyTransient(ctx context.Context, userID
return m.pushToBusinessAuthKeyBestEffort(ctx, userID, businessAuthKeyID, 0, t, msg, timeout)
}
func (m *SessionManager) PushToUserAuthKeyTransientAtLeastLayer(ctx context.Context, userID int64, businessAuthKeyID [8]byte, minLayer int, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) {
return m.pushToBusinessAuthKeyBestEffort(ctx, userID, businessAuthKeyID, minLayer, t, msg, timeout)
func (m *SessionManager) PushToUserAuthKeyTransientCompatible(ctx context.Context, userID int64, businessAuthKeyID [8]byte, semantic tlprofile.SemanticID, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) {
return m.pushToBusinessAuthKeyBestEffort(ctx, userID, businessAuthKeyID, semantic, t, msg, timeout)
}
func (m *SessionManager) pushToBusinessAuthKeyBestEffort(ctx context.Context, userID int64, businessAuthKeyID [8]byte, minLayer int, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) {
// PushToUserExceptBusinessAuthKey 把 update 投给账号其它设备,精确排除同一 permanent
// business auth key 下的所有 raw/temp/PFS 连接。密聊 accept 用它让输掉竞态的设备收敛为
// discarded,同时保证获胜设备的其它连接不会误删刚建立的密聊。
func (m *SessionManager) PushToUserExceptBusinessAuthKey(ctx context.Context, userID int64, excludeBusinessAuthKeyID [8]byte, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) {
getUpdates := onceLayerUpdatesFanout(ctx, msg)
return m.pushToUserWithSender(ctx, userID, nil, 0, &excludeBusinessAuthKeyID, 0, t, getUpdates, false, func(c *Conn) error {
if c.outbound == nil || c.outboundControl == nil {
return ErrConnClosed
}
updates, err := getUpdates()
if err != nil {
return err
}
encoded, err := updates.prepareForConn(ctx, c)
if err != nil {
return err
}
return c.SendBestEffortEncoded(ctx, t, encoded, timeout)
})
}
func (m *SessionManager) pushToBusinessAuthKeyBestEffort(ctx context.Context, userID int64, businessAuthKeyID [8]byte, semantic tlprofile.SemanticID, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) {
if ctx != nil && ctx.Err() != nil {
return 0, ctx.Err()
}
@ -1497,7 +1655,7 @@ func (m *SessionManager) pushToBusinessAuthKeyBestEffort(ctx context.Context, us
defer cancel()
}
getUpdates := onceLayerUpdatesFanout(sendCtx, msg)
return m.pushToBusinessAuthKey(ctx, userID, businessAuthKeyID, minLayer, func(c *Conn) error {
return m.pushToBusinessAuthKey(ctx, userID, businessAuthKeyID, semantic, func(c *Conn) error {
if c.outbound == nil || c.outboundControl == nil {
return ErrConnClosed
}
@ -1520,7 +1678,7 @@ func (m *SessionManager) pushToBusinessAuthKeyBestEffort(ctx context.Context, us
})
}
func (m *SessionManager) pushToBusinessAuthKey(ctx context.Context, userID int64, businessAuthKeyID [8]byte, minLayer int, send func(*Conn) error) (int, error) {
func (m *SessionManager) pushToBusinessAuthKey(ctx context.Context, userID int64, businessAuthKeyID [8]byte, semantic tlprofile.SemanticID, send func(*Conn) error) (int, error) {
m.mu.Lock()
candidates := m.businessAuthKeyCandidatesLocked(businessAuthKeyID)
conns := make([]*Conn, 0, len(candidates))
@ -1532,7 +1690,7 @@ func (m *SessionManager) pushToBusinessAuthKey(ctx context.Context, userID int64
// 未就绪:密聊消息靠 getDifference 补,typing 直接丢——都不进 pending。
continue
}
if !sessionSupportsMinimumLayer(c, minLayer) {
if !sessionSupportsSemantic(c, semantic) {
continue
}
conns = append(conns, c)
@ -1579,7 +1737,7 @@ func (m *SessionManager) pushToBusinessAuthKey(ctx context.Context, userID int64
func (m *SessionManager) pushToUser(ctx context.Context, userID int64, excludeAuthKeyID *[8]byte, excludeSessionID int64, t proto.MessageType, msg tg.UpdatesClass) (int, error) {
getUpdates := onceLayerUpdatesFanout(ctx, msg)
return m.pushToUserWithSender(ctx, userID, excludeAuthKeyID, excludeSessionID, 0, t, getUpdates, true, func(c *Conn) error {
return m.pushToUserWithSender(ctx, userID, excludeAuthKeyID, excludeSessionID, nil, 0, t, getUpdates, true, func(c *Conn) error {
if c.outbound == nil || c.outboundControl == nil {
return ErrConnClosed
}
@ -1602,7 +1760,7 @@ func (m *SessionManager) pushToUser(ctx context.Context, userID int64, excludeAu
// 「durable 兜底」丢弃。走 best-effort 发送,不阻塞调用方。
func (m *SessionManager) PushToUserTransientExceptAuthKeySession(ctx context.Context, userID int64, excludeAuthKeyID [8]byte, excludeSessionID int64, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) {
getUpdates := onceLayerUpdatesFanout(ctx, msg)
return m.pushToUserWithSender(ctx, userID, &excludeAuthKeyID, excludeSessionID, 0, t, getUpdates, false, func(c *Conn) error {
return m.pushToUserWithSender(ctx, userID, &excludeAuthKeyID, excludeSessionID, nil, 0, t, getUpdates, false, func(c *Conn) error {
if c.outbound == nil || c.outboundControl == nil {
return ErrConnClosed
}
@ -1618,9 +1776,9 @@ func (m *SessionManager) PushToUserTransientExceptAuthKeySession(ctx context.Con
})
}
func (m *SessionManager) PushToUserTransientAtLeastLayer(ctx context.Context, userID int64, minLayer int, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) {
func (m *SessionManager) PushToUserTransientCompatible(ctx context.Context, userID int64, semantic tlprofile.SemanticID, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) {
getUpdates := onceLayerUpdatesFanout(ctx, msg)
return m.pushToUserWithSender(ctx, userID, nil, 0, minLayer, t, getUpdates, false, func(c *Conn) error {
return m.pushToUserWithSender(ctx, userID, nil, 0, nil, semantic, t, getUpdates, false, func(c *Conn) error {
if c.outbound == nil || c.outboundControl == nil {
return ErrConnClosed
}
@ -1641,10 +1799,6 @@ func (m *SessionManager) PushToUserExceptAuthKeySessionBestEffort(ctx context.Co
}
func (m *SessionManager) pushToUserBestEffort(ctx context.Context, userID int64, excludeAuthKeyID *[8]byte, excludeSessionID int64, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) {
return m.pushToUserBestEffortAtLeastLayer(ctx, userID, excludeAuthKeyID, excludeSessionID, 0, t, msg, timeout)
}
func (m *SessionManager) pushToUserBestEffortAtLeastLayer(ctx context.Context, userID int64, excludeAuthKeyID *[8]byte, excludeSessionID int64, minLayer int, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error) {
if ctx != nil && ctx.Err() != nil {
return 0, ctx.Err()
}
@ -1670,7 +1824,7 @@ func (m *SessionManager) pushToUserBestEffortAtLeastLayer(ctx context.Context, u
defer cancel()
}
getUpdates := onceLayerUpdatesFanout(sendCtx, msg)
return m.pushToUserWithSender(ctx, userID, excludeAuthKeyID, excludeSessionID, minLayer, t, getUpdates, true, func(c *Conn) error {
return m.pushToUserWithSender(ctx, userID, excludeAuthKeyID, excludeSessionID, nil, 0, t, getUpdates, true, func(c *Conn) error {
if c.outbound == nil || c.outboundControl == nil {
return ErrConnClosed
}
@ -1717,7 +1871,7 @@ func onceLayerUpdatesFanout(ctx context.Context, msg tg.UpdatesClass) func() (*l
}
}
func (m *SessionManager) pushToUserWithSender(ctx context.Context, userID int64, excludeAuthKeyID *[8]byte, excludeSessionID int64, minLayer int, t proto.MessageType, getUpdates func() (*layerUpdatesFanout, error), queueWhenNotReady bool, send func(*Conn) error) (int, error) {
func (m *SessionManager) pushToUserWithSender(ctx context.Context, userID int64, excludeAuthKeyID *[8]byte, excludeSessionID int64, excludeBusinessAuthKeyID *[8]byte, semantic tlprofile.SemanticID, t proto.MessageType, getUpdates func() (*layerUpdatesFanout, error), queueWhenNotReady bool, send func(*Conn) error) (int, error) {
// push fan-out 是连接层最热路径之一:debug 日志的字段构造(含 auth_key hex 格式化)
// 在关闭 debug 时也会求值,先查级别一次、按需记日志。
debug := m.log.Core().Enabled(zapcore.DebugLevel)
@ -1733,11 +1887,11 @@ func (m *SessionManager) pushToUserWithSender(ctx context.Context, userID int64,
skipped := 0
needQueue := false
for key, c := range m.byUser[userID] {
if shouldExcludeSession(c, excludeAuthKeyID, excludeSessionID) {
if shouldExcludeSession(c, excludeAuthKeyID, excludeSessionID) || shouldExcludeBusinessAuthKey(c, excludeBusinessAuthKeyID) {
excluded++
continue
}
if !sessionSupportsMinimumLayer(c, minLayer) {
if !sessionSupportsSemantic(c, semantic) {
skipped++
continue
}
@ -1768,11 +1922,11 @@ func (m *SessionManager) pushToUserWithSender(ctx context.Context, userID int64,
m.mu.Lock()
total = len(m.byUser[userID])
for key, c := range m.byUser[userID] {
if shouldExcludeSession(c, excludeAuthKeyID, excludeSessionID) {
if shouldExcludeSession(c, excludeAuthKeyID, excludeSessionID) || shouldExcludeBusinessAuthKey(c, excludeBusinessAuthKeyID) {
excluded++
continue
}
if !sessionSupportsMinimumLayer(c, minLayer) {
if !sessionSupportsSemantic(c, semantic) {
skipped++
continue
}
@ -1825,6 +1979,9 @@ func (m *SessionManager) pushToUserWithSender(ctx context.Context, userID int64,
if c.userID.Load() != userID {
continue
}
if shouldExcludeBusinessAuthKey(c, excludeBusinessAuthKeyID) {
continue
}
if err := send(c); err != nil {
if isOutboundStaleLayerEpoch(err) {
// Do not classify profile correction as slow-consumer evidence.
@ -2330,6 +2487,7 @@ func (m *SessionManager) removeLocked(c *Conn, dropPending bool) int64 {
removeUserIndex(m.byUser, uid, key)
}
m.clearSessionChannelIndexesLocked(c, key)
m.clearUpdatesActivationLocked(c)
if dropPending {
m.deletePendingLocked(key)
}
@ -2783,15 +2941,26 @@ func shouldExcludeSession(c *Conn, excludeAuthKeyID *[8]byte, excludeSessionID i
return c.authKeyID == *excludeAuthKeyID
}
func sessionSupportsMinimumLayer(c *Conn, minLayer int) bool {
if minLayer <= 0 {
func shouldExcludeBusinessAuthKey(c *Conn, excludeBusinessAuthKeyID *[8]byte) bool {
if c == nil || excludeBusinessAuthKeyID == nil || *excludeBusinessAuthKeyID == ([8]byte{}) {
return false
}
return connUsesBusinessAuthKey(c, *excludeBusinessAuthKeyID)
}
func sessionSupportsSemantic(c *Conn, semantic tlprofile.SemanticID) bool {
if semantic == 0 {
return true
}
if c == nil {
return false
}
state := c.LayerProfileState()
return state.Origin != LayerProfileUnknown && int(state.Profile) >= minLayer
if state.Origin == LayerProfileUnknown {
return false
}
_, ok := tlprofile.WireID(state.Profile, semantic)
return ok
}
func sessionKeyLog(id [8]byte) string {