361 lines
11 KiB
Go
361 lines
11 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/postresponse"
|
|
)
|
|
|
|
const updatesDeliveryPhaseTimeout = 5 * time.Second
|
|
|
|
type updatesDeliveryPlanKey struct{}
|
|
|
|
// updatesDeliveryPlan collects every state transition that is justified by one
|
|
// successful RPC result. Dispatch owns the plan and registers exactly one
|
|
// post-response callback after the typed handler has succeeded. This keeps the
|
|
// baseline result ahead of pending updates on the physical MTProto stream and
|
|
// gives secret-chat delivery, session readiness and bootstrap publication one
|
|
// explicit order.
|
|
type updatesDeliveryPlan struct {
|
|
baseCtx context.Context
|
|
|
|
commitCursor bool
|
|
cursorAuthKey [8]byte
|
|
cursorUserID int64
|
|
cursorState domain.UpdateState
|
|
cursorMode domain.UpdateStateCommitMode
|
|
|
|
markSecretDelivered bool
|
|
secretDeviceKey int64
|
|
secretEventIDs []int64
|
|
|
|
markSessionReady bool
|
|
readyUserID int64
|
|
activation SessionUpdatesActivationProvider
|
|
activationRaw [8]byte
|
|
activationSess int64
|
|
activationToken uint64
|
|
|
|
publishBootstrap bool
|
|
bootstrapUserID int64
|
|
bootstrapProbe SessionBootstrapProbeProvider
|
|
bootstrapRaw [8]byte
|
|
bootstrapSess int64
|
|
bootstrapToken uint64
|
|
}
|
|
|
|
func withUpdatesDeliveryPlan(ctx context.Context) (context.Context, *updatesDeliveryPlan) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
plan := &updatesDeliveryPlan{baseCtx: context.WithoutCancel(ctx)}
|
|
return context.WithValue(ctx, updatesDeliveryPlanKey{}, plan), plan
|
|
}
|
|
|
|
func updatesDeliveryPlanFrom(ctx context.Context) (*updatesDeliveryPlan, bool) {
|
|
if ctx == nil {
|
|
return nil, false
|
|
}
|
|
plan, ok := ctx.Value(updatesDeliveryPlanKey{}).(*updatesDeliveryPlan)
|
|
return plan, ok && plan != nil
|
|
}
|
|
|
|
func (p *updatesDeliveryPlan) hasWork() bool {
|
|
return p != nil && (p.commitCursor || p.markSecretDelivered || p.markSessionReady || p.publishBootstrap)
|
|
}
|
|
|
|
func (p *updatesDeliveryPlan) stageCursor(authKeyID [8]byte, userID int64, state domain.UpdateState, mode domain.UpdateStateCommitMode) {
|
|
if p == nil || userID == 0 {
|
|
return
|
|
}
|
|
p.commitCursor = true
|
|
p.cursorAuthKey = authKeyID
|
|
p.cursorUserID = userID
|
|
p.cursorState = state
|
|
p.cursorMode = mode
|
|
}
|
|
|
|
func (p *updatesDeliveryPlan) stageSessionReady(userID int64) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
p.markSessionReady = true
|
|
p.readyUserID = userID
|
|
}
|
|
|
|
func (p *updatesDeliveryPlan) ownSessionActivation(provider SessionUpdatesActivationProvider, rawAuthKeyID [8]byte, sessionID int64, token uint64) {
|
|
if p == nil || provider == nil || token == 0 {
|
|
return
|
|
}
|
|
p.activation = provider
|
|
p.activationRaw = rawAuthKeyID
|
|
p.activationSess = sessionID
|
|
p.activationToken = token
|
|
}
|
|
|
|
func (p *updatesDeliveryPlan) disownSessionActivation() {
|
|
if p == nil {
|
|
return
|
|
}
|
|
p.activation = nil
|
|
p.activationRaw = [8]byte{}
|
|
p.activationSess = 0
|
|
p.activationToken = 0
|
|
}
|
|
|
|
func (p *updatesDeliveryPlan) releaseSessionActivation() {
|
|
if p == nil || p.activation == nil || p.activationToken == 0 {
|
|
return
|
|
}
|
|
provider := p.activation
|
|
rawAuthKeyID := p.activationRaw
|
|
sessionID := p.activationSess
|
|
token := p.activationToken
|
|
p.disownSessionActivation()
|
|
provider.EndSessionUpdatesActivation(rawAuthKeyID, sessionID, token)
|
|
}
|
|
|
|
func (p *updatesDeliveryPlan) ownBootstrapProbe(provider SessionBootstrapProbeProvider, rawAuthKeyID [8]byte, sessionID int64, token uint64) {
|
|
if p == nil || provider == nil || token == 0 {
|
|
return
|
|
}
|
|
p.bootstrapProbe = provider
|
|
p.bootstrapRaw = rawAuthKeyID
|
|
p.bootstrapSess = sessionID
|
|
p.bootstrapToken = token
|
|
}
|
|
|
|
func (p *updatesDeliveryPlan) finishBootstrapProbe(success bool) {
|
|
if p == nil || p.bootstrapProbe == nil || p.bootstrapToken == 0 {
|
|
return
|
|
}
|
|
provider := p.bootstrapProbe
|
|
rawAuthKeyID := p.bootstrapRaw
|
|
sessionID := p.bootstrapSess
|
|
token := p.bootstrapToken
|
|
p.bootstrapProbe = nil
|
|
p.bootstrapRaw = [8]byte{}
|
|
p.bootstrapSess = 0
|
|
p.bootstrapToken = 0
|
|
provider.EndSessionBootstrapProbe(rawAuthKeyID, sessionID, token, success)
|
|
}
|
|
|
|
// suppressSessionActivation removes only effects which would make the current
|
|
// physical session eligible for proactive updates. Delivery-gated cursor and
|
|
// secret-event facts remain valid for a generated wire-invariant RPC result.
|
|
// This is used when exact admission has no authoritative profile evidence.
|
|
func (p *updatesDeliveryPlan) suppressSessionActivation() {
|
|
if p == nil {
|
|
return
|
|
}
|
|
p.releaseSessionActivation()
|
|
p.finishBootstrapProbe(false)
|
|
p.markSessionReady = false
|
|
p.readyUserID = 0
|
|
p.publishBootstrap = false
|
|
p.bootstrapUserID = 0
|
|
}
|
|
|
|
func (r *Router) tryStageBootstrapProbe(ctx context.Context, plan *updatesDeliveryPlan, userID int64) {
|
|
if plan == nil || userID == 0 || plan.publishBootstrap || r.deps.BootstrapUpdates == nil {
|
|
return
|
|
}
|
|
if provider, ok := r.deps.Sessions.(SessionBootstrapProbeProvider); ok {
|
|
rawAuthKeyID, hasRaw := RawAuthKeyIDFrom(ctx)
|
|
sessionID, hasSession := SessionIDFrom(ctx)
|
|
if hasRaw && hasSession {
|
|
token, claimed := provider.BeginSessionBootstrapProbe(rawAuthKeyID, sessionID)
|
|
if !claimed {
|
|
return
|
|
}
|
|
plan.ownBootstrapProbe(provider, rawAuthKeyID, sessionID, token)
|
|
}
|
|
}
|
|
plan.publishBootstrap = true
|
|
plan.bootstrapUserID = userID
|
|
}
|
|
|
|
func (p *updatesDeliveryPlan) stageBaseline(secretDeviceKey int64, secretEventIDs []int64) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
if secretDeviceKey != 0 && len(secretEventIDs) != 0 {
|
|
p.markSecretDelivered = true
|
|
p.secretDeviceKey = secretDeviceKey
|
|
p.secretEventIDs = appendUniqueInt64s(p.secretEventIDs, secretEventIDs...)
|
|
}
|
|
}
|
|
|
|
func appendUniqueInt64s(dst []int64, values ...int64) []int64 {
|
|
for _, value := range values {
|
|
if value == 0 {
|
|
continue
|
|
}
|
|
seen := false
|
|
for _, existing := range dst {
|
|
if existing == value {
|
|
seen = true
|
|
break
|
|
}
|
|
}
|
|
if !seen {
|
|
dst = append(dst, value)
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func (p *updatesDeliveryPlan) snapshot() updatesDeliveryPlan {
|
|
if p == nil {
|
|
return updatesDeliveryPlan{}
|
|
}
|
|
out := *p
|
|
out.secretEventIDs = append([]int64(nil), p.secretEventIDs...)
|
|
return out
|
|
}
|
|
|
|
// stageSessionUpdatesReadyAfterDelivery is used by ordinary bare RPCs. During
|
|
// Router.Dispatch it only mutates the request-owned plan; direct handler tests
|
|
// with a postresponse registry still get the same delivery-gated behavior.
|
|
func (r *Router) stageSessionUpdatesReadyAfterDelivery(ctx context.Context, userID int64) {
|
|
if userID == 0 || r.deps.Sessions == nil {
|
|
return
|
|
}
|
|
if plan, ok := updatesDeliveryPlanFrom(ctx); ok {
|
|
r.tryStageSessionUpdatesReady(ctx, plan, userID)
|
|
return
|
|
}
|
|
plan := updatesDeliveryPlan{baseCtx: context.WithoutCancel(ctx)}
|
|
r.tryStageSessionUpdatesReady(ctx, &plan, userID)
|
|
r.registerUpdatesDeliveryPlan(ctx, &plan)
|
|
}
|
|
|
|
func (r *Router) tryStageSessionUpdatesReady(ctx context.Context, plan *updatesDeliveryPlan, userID int64) {
|
|
if plan == nil || userID == 0 || plan.markSessionReady {
|
|
return
|
|
}
|
|
if provider, ok := r.deps.Sessions.(SessionUpdatesActivationProvider); ok {
|
|
rawAuthKeyID, hasRaw := RawAuthKeyIDFrom(ctx)
|
|
sessionID, hasSession := SessionIDFrom(ctx)
|
|
if hasRaw && hasSession {
|
|
token, claimed := provider.BeginSessionUpdatesActivation(rawAuthKeyID, sessionID)
|
|
if !claimed {
|
|
return
|
|
}
|
|
plan.ownSessionActivation(provider, rawAuthKeyID, sessionID, token)
|
|
}
|
|
}
|
|
plan.stageSessionReady(userID)
|
|
}
|
|
|
|
// stageUpdatesBaselineAfterDelivery adds the extra actions justified by a
|
|
// successful getState/getDifference result. A single plan also deduplicates the
|
|
// ordinary bare-RPC readiness declaration made by the common router path.
|
|
func (r *Router) stageUpdatesBaselineAfterDelivery(
|
|
ctx context.Context,
|
|
userID int64,
|
|
cursor *domain.UpdateState,
|
|
mode domain.UpdateStateCommitMode,
|
|
secretEventIDs []int64,
|
|
bootstrap bool,
|
|
) {
|
|
var secretDeviceKey int64
|
|
if len(secretEventIDs) != 0 {
|
|
secretDeviceKey, _ = businessAuthKeyIDFrom(ctx)
|
|
}
|
|
subscribe := !invokeWithoutUpdatesFrom(ctx)
|
|
stage := func(plan *updatesDeliveryPlan) {
|
|
if cursor != nil && r.deps.Updates != nil {
|
|
authKeyID, _ := AuthKeyIDFrom(ctx)
|
|
plan.stageCursor(authKeyID, userID, *cursor, mode)
|
|
}
|
|
plan.stageBaseline(secretDeviceKey, secretEventIDs)
|
|
if subscribe {
|
|
r.tryStageSessionUpdatesReady(ctx, plan, userID)
|
|
if bootstrap && userID != 0 {
|
|
r.tryStageBootstrapProbe(ctx, plan, userID)
|
|
}
|
|
}
|
|
}
|
|
if plan, ok := updatesDeliveryPlanFrom(ctx); ok {
|
|
stage(plan)
|
|
return
|
|
}
|
|
plan := updatesDeliveryPlan{baseCtx: context.WithoutCancel(ctx)}
|
|
stage(&plan)
|
|
r.registerUpdatesDeliveryPlan(ctx, &plan)
|
|
}
|
|
|
|
func (r *Router) registerUpdatesDeliveryPlan(ctx context.Context, plan *updatesDeliveryPlan) {
|
|
if plan == nil {
|
|
return
|
|
}
|
|
if !plan.hasWork() {
|
|
plan.releaseSessionActivation()
|
|
plan.finishBootstrapProbe(false)
|
|
return
|
|
}
|
|
snapshot := plan.snapshot()
|
|
if !postresponse.Register(ctx, func() {
|
|
r.runUpdatesDeliveryPlan(snapshot)
|
|
}) {
|
|
snapshot.releaseSessionActivation()
|
|
snapshot.finishBootstrapProbe(false)
|
|
return
|
|
}
|
|
plan.disownSessionActivation()
|
|
}
|
|
|
|
// runUpdatesDeliveryPlan is ordered deliberately:
|
|
// 1. commit the exact account cursor carried by the delivered result;
|
|
// 2. the just-delivered difference may retire its projected secret-chat events;
|
|
// 3. membership routing is rebuilt before SetReceivesUpdates starts FIFO flush;
|
|
// 4. bootstrap jobs are published last, so they queue behind older pending updates.
|
|
//
|
|
// Each phase gets an independent timeout so one failed side effect cannot starve
|
|
// the remaining delivery-safe transitions.
|
|
func (r *Router) runUpdatesDeliveryPlan(plan updatesDeliveryPlan) {
|
|
defer plan.releaseSessionActivation()
|
|
defer plan.finishBootstrapProbe(false)
|
|
baseCtx := plan.baseCtx
|
|
if baseCtx == nil {
|
|
baseCtx = context.Background()
|
|
}
|
|
if plan.commitCursor && r.deps.Updates != nil && plan.cursorUserID != 0 {
|
|
ctx, cancel := context.WithTimeout(baseCtx, updatesDeliveryPhaseTimeout)
|
|
err := r.deps.Updates.CommitDeliveredState(ctx, plan.cursorAuthKey, plan.cursorUserID, plan.cursorState, plan.cursorMode)
|
|
cancel()
|
|
if err != nil {
|
|
r.log.Warn("commit delivered update state after rpc_result",
|
|
zap.Int64("user_id", plan.cursorUserID),
|
|
zap.Int("pts", plan.cursorState.Pts),
|
|
zap.Uint8("mode", uint8(plan.cursorMode)),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
if plan.markSecretDelivered && r.deps.SecretChats != nil && plan.secretDeviceKey != 0 && len(plan.secretEventIDs) != 0 {
|
|
ctx, cancel := context.WithTimeout(baseCtx, updatesDeliveryPhaseTimeout)
|
|
err := r.deps.SecretChats.MarkStateEventsDelivered(ctx, plan.secretDeviceKey, plan.secretEventIDs)
|
|
cancel()
|
|
if err != nil {
|
|
r.log.Warn("mark encrypted state events delivered after rpc_result",
|
|
zap.Int64("device_auth_key_id", plan.secretDeviceKey),
|
|
zap.Int("event_count", len(plan.secretEventIDs)),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
if plan.markSessionReady {
|
|
ctx, cancel := context.WithTimeout(baseCtx, updatesDeliveryPhaseTimeout)
|
|
r.markSessionReceivesUpdatesNow(ctx, plan.readyUserID)
|
|
cancel()
|
|
}
|
|
if plan.publishBootstrap {
|
|
if r.publishBootstrapAfterBaseline(baseCtx, plan.bootstrapUserID) {
|
|
plan.finishBootstrapProbe(true)
|
|
}
|
|
}
|
|
}
|