package rpc import ( "context" "errors" "fmt" "github.com/iamxvbaba/td/tlprofile" "telesrv/internal/store" ) type layerEvidenceDurabilityUnavailableError struct { cause error } func (e *layerEvidenceDurabilityUnavailableError) Error() string { return fmt.Sprintf("durable Layer evidence unavailable: %v", e.cause) } func (e *layerEvidenceDurabilityUnavailableError) Unwrap() error { return e.cause } // LayerEvidenceDurabilityUnavailable is a structural marker consumed by the // lower MTProto edge without importing rpc. It permits an explicit selector to // remain connection-local during a transient store outage; conflicts and // missing/destroyed auth keys never use this availability fallback. func (*layerEvidenceDurabilityUnavailableError) LayerEvidenceDurabilityUnavailable() {} func wrapLayerEvidenceStoreAvailability(err error) error { if err == nil || errors.Is(err, store.ErrAuthKeySessionLayerConflict) || errors.Is(err, store.ErrAuthKeySessionLayerInvalid) || errors.Is(err, store.ErrAuthKeyNotFound) || errors.Is(err, store.ErrAuthKeyBindingInvalid) { return err } return &layerEvidenceDurabilityUnavailableError{cause: err} } // ResolveNegotiatedSessionLayerEvidence is the restart-safe resolver used by // the MTProto edge on a physical connection's first exact admission. When a // durable store is configured it is authoritative on every resolve: another // Router process may have advanced the same logical session since this // process cached it. The local registry is only the no-store implementation or // an availability fallback. A durable future Layer is returned verbatim but // is not installed into the old binary's typed registry; the edge keeps its raw // msg_id watermark so a newer supported invokeWithLayer can self-heal. func (r *Router) ResolveNegotiatedSessionLayerEvidence( ctx context.Context, rawAuthKeyID [8]byte, sessionID int64, ) (layer int, msgID int64, found bool, err error) { if r == nil || rawAuthKeyID == ([8]byte{}) || sessionID == 0 { return 0, 0, false, nil } if r.deps.AuthKeySessionLayers == nil { layer, msgID, found := r.NegotiatedSessionLayerEvidence(rawAuthKeyID, sessionID) return layer, msgID, found, nil } localLayer, localMsgID, localFound := r.NegotiatedSessionLayerEvidence(rawAuthKeyID, sessionID) value, found, err := r.deps.AuthKeySessionLayers.GetSessionLayer(ctx, rawAuthKeyID, sessionID) if err != nil { if localFound { // This exact process observed the selector earlier. It is weaker than // primary and is refreshed as soon as the store recovers, but remains a // safe same-session availability fallback. Fresh explicit evidence still // goes through durable Advance and becomes connection-local on failure. return localLayer, localMsgID, true, nil } return 0, 0, false, wrapLayerEvidenceStoreAvailability(err) } if !found { r.forgetCachedDurableSessionLayer(rawAuthKeyID, sessionID) return 0, 0, false, nil } if value.MessageID <= 0 || value.Layer <= 0 || value.ObservationID <= 0 { return 0, 0, false, store.ErrAuthKeySessionLayerInvalid } if err := r.cacheResolvedDurableSessionLayer(rawAuthKeyID, sessionID, value); err != nil { return 0, 0, false, err } return value.Layer, value.MessageID, true, nil } // AdvanceNegotiatedSessionLayerEvidence commits the same-session watermark and // auth-key-wide default atomically before any connection/profile/readiness // state is mutated. publishShared is true only for a different durable profile // generation which still owns the shared default. A same-generation msg_id // advance remains fully durable and fresh for the exact session, but does not // re-read/re-publish the unchanged auth-key default. func (r *Router) AdvanceNegotiatedSessionLayerEvidence( ctx context.Context, rawAuthKeyID [8]byte, sessionID int64, layer int, msgID int64, ) (currentLayer int, currentMsgID int64, publishShared bool, err error) { if r == nil || rawAuthKeyID == ([8]byte{}) || sessionID == 0 || msgID <= 0 { return 0, 0, false, store.ErrAuthKeySessionLayerInvalid } if r.deps.AuthKeySessionLayers == nil { if _, err := r.FreezeNegotiatedSessionLayerAt(rawAuthKeyID, sessionID, layer, msgID); err != nil { return 0, 0, false, err } currentLayer, currentMsgID, found := r.NegotiatedSessionLayerEvidence(rawAuthKeyID, sessionID) if !found { return 0, 0, false, fmt.Errorf("exact session Layer disappeared after in-memory advance") } return currentLayer, currentMsgID, currentLayer == layer && currentMsgID == msgID, nil } previousObservationID, hadPreviousGeneration := r.cachedDurableSessionLayerObservation(rawAuthKeyID, sessionID) current, _, err := r.deps.AuthKeySessionLayers.AdvanceSessionLayer( ctx, rawAuthKeyID, sessionID, layer, msgID, ) if err != nil { if errors.Is(err, store.ErrAuthKeySessionLayerConflict) { return current.Layer, current.MessageID, false, err } return 0, 0, false, wrapLayerEvidenceStoreAvailability(err) } if current.ObservationID <= 0 { return 0, 0, false, store.ErrAuthKeySessionLayerInvalid } if err := r.cacheResolvedDurableSessionLayer(rawAuthKeyID, sessionID, current); err != nil { return 0, 0, false, err } generationChanged := !hadPreviousGeneration || previousObservationID != current.ObservationID return current.Layer, current.MessageID, current.SharedDefault && generationChanged, nil } func (r *Router) cachedDurableSessionLayerObservation(rawAuthKeyID [8]byte, sessionID int64) (int64, bool) { if r == nil || rawAuthKeyID == ([8]byte{}) || sessionID == 0 { return 0, false } key := clientInfoSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID} now := r.clock.Now() r.exactProfileMu.RLock() entry, found := r.exactProfiles[key] r.exactProfileMu.RUnlock() if !found || entry.observationID <= 0 || !now.Before(entry.expiresAt) { return 0, false } return entry.observationID, true } // cacheResolvedDurableSessionLayer updates only the bounded typed accelerator; // callers always return the store row itself as authority. Ordered replacement // prevents an older read, which linearized immediately before a concurrent // Advance, from rolling this process cache back after that Advance completed. func (r *Router) cacheResolvedDurableSessionLayer( rawAuthKeyID [8]byte, sessionID int64, value store.AuthKeySessionLayer, ) error { if r == nil { return nil } key := clientInfoSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID} profile, supported := tlprofile.ResolveProfile(value.Layer) if !supported || int(profile) != value.Layer { // A newer binary may have persisted a future profile. Never leave an old // typed codec shadow beside that raw authoritative watermark. Observation // order still protects a concurrent newer supported Advance from an older // in-flight read of this future row. r.exactProfileMu.Lock() if current, exists := r.exactProfiles[key]; exists && r.clock.Now().Before(current.expiresAt) { if current.observationID > value.ObservationID && value.ObservationID > 0 { r.exactProfileMu.Unlock() return nil } if current.observationID == value.ObservationID && value.ObservationID > 0 { r.exactProfileMu.Unlock() return fmt.Errorf("%w: observation %d maps to supported Layer %d and raw Layer %d", store.ErrAuthKeySessionLayerConflict, value.ObservationID, current.layer, value.Layer) } } delete(r.exactProfiles, key) r.exactProfileMu.Unlock() return nil } now := r.clock.Now() expiresAt := value.ExpiresAt if expiresAt.IsZero() { expiresAt = exactSessionLayerEvidenceExpiry(now, value.MessageID) } entry := exactSessionProfileEntry{ layer: value.Layer, msgID: value.MessageID, observationID: value.ObservationID, expiresAt: expiresAt, } r.exactProfileMu.Lock() defer r.exactProfileMu.Unlock() if r.exactProfiles == nil { r.exactProfiles = make(map[clientInfoSessionKey]exactSessionProfileEntry) } if current, exists := r.exactProfiles[key]; exists && now.Before(current.expiresAt) { switch { case current.observationID > entry.observationID && entry.observationID > 0: // This DB read linearized immediately before a concurrent durable // Advance which already refreshed the local cache. Do not roll it back. return nil case current.observationID == entry.observationID && entry.observationID > 0: if current.layer != entry.layer { return fmt.Errorf("%w: observation %d maps to Layer %d and %d", store.ErrAuthKeySessionLayerConflict, entry.observationID, current.layer, entry.layer) } if current.msgID > entry.msgID { // One same-Layer fast advance refreshed this process after the // current DB read linearized. Observation identifies the stable // Layer generation; msg_id remains its monotonic high-water mark. return nil } // The store row may have a newer same-generation high-water mark or // an authoritative expiry refresh; replace it. case entry.observationID <= 0 && current.msgID > entry.msgID: // Defensive compatibility for an old custom store without observation // ids. Production stores always take the branches above. return nil } } if _, exists := r.exactProfiles[key]; !exists && len(r.exactProfiles) >= maxExactSessionProfileEntries { r.purgeExpiredExactSessionProfilesLocked(now) if len(r.exactProfiles) >= maxExactSessionProfileEntries { // Durable primary remains authoritative; skipping this accelerator can // never reject the request or lose the raw watermark returned to edge. return nil } } r.exactProfiles[key] = entry if len(r.exactProfiles) == 1 { r.exactProfileEarliestExpiry = entry.expiresAt } else { r.noteExactSessionProfileExpiryLocked(entry.expiresAt) } return nil } func (r *Router) forgetCachedDurableSessionLayer(rawAuthKeyID [8]byte, sessionID int64) { if r == nil { return } r.exactProfileMu.Lock() delete(r.exactProfiles, clientInfoSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID}) r.exactProfileMu.Unlock() } // DeleteNegotiatedSessionLayerEvidence is the durable destroy_session // boundary. The edge must complete this call before acknowledging destruction; // ordinary transport disconnect never invokes it. func (r *Router) DeleteNegotiatedSessionLayerEvidence( ctx context.Context, rawAuthKeyID [8]byte, sessionID int64, ) (bool, error) { if r == nil || rawAuthKeyID == ([8]byte{}) || sessionID == 0 { return false, nil } _, _, inMemory := r.NegotiatedSessionLayerEvidence(rawAuthKeyID, sessionID) deleted := false if r.deps.AuthKeySessionLayers != nil { var err error deleted, err = r.deps.AuthKeySessionLayers.DeleteSessionLayer(ctx, rawAuthKeyID, sessionID) if err != nil { return false, err } } r.ForgetNegotiatedSessionLayer(rawAuthKeyID, sessionID) return deleted || inMemory, nil }