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

@ -11,11 +11,10 @@ import (
)
const (
dialogLightReadModel = readmodel.ModelDialogLight
channelBaseReadModel = readmodel.ModelChannelBase
channelMemberReadModel = readmodel.ModelChannelMember
defaultDialogPeerReadModelTTL = 24 * time.Hour
dialogPeerReadModelMaxEntries = 8192
dialogLightReadModel = readmodel.ModelDialogLight
defaultDialogPeerReadModelTTL = 24 * time.Hour
defaultDialogPeerReadModelMaxEntries = 500000
defaultDialogPeerReadModelMaxBytes int64 = 256 << 20
)
type dialogPeerCacheKey struct {
@ -32,12 +31,18 @@ type dialogPeerReadModelCache struct {
}
func newDialogPeerReadModelCache(ttl time.Duration) *dialogPeerReadModelCache {
return newDialogPeerReadModelCacheWithLimits(defaultDialogPeerReadModelMaxEntries, defaultDialogPeerReadModelMaxBytes, ttl)
}
func newDialogPeerReadModelCacheWithLimits(maxEntries int, maxBytes int64, ttl time.Duration) *dialogPeerReadModelCache {
if ttl <= 0 {
ttl = defaultDialogPeerReadModelTTL
}
return &dialogPeerReadModelCache{
cache: readmodelcache.New[dialogPeerCacheKey, domain.DialogList](readmodelcache.Config[dialogPeerCacheKey, domain.DialogList]{
MaxEntries: dialogPeerReadModelMaxEntries,
MaxEntries: maxEntries,
MaxWeight: maxBytes,
Weight: dialogPeerListApproxBytes,
TTL: ttl,
Clone: cloneDialogList,
}),
@ -52,7 +57,7 @@ func (s *Service) userPeerDialogsReadModel(ctx context.Context, userID int64, pe
if len(unique) == 0 {
return domain.DialogList{}, nil
}
return s.cachedPeerDialogsReadModel(ctx, userID, unique, s.userDialogHashes, s.loadUserPeerDialogs)
return s.cachedPeerDialogsReadModel(ctx, userID, unique, s.dialogHashes, s.loadUserPeerDialogs)
}
func (s *Service) channelPeerDialogsReadModel(ctx context.Context, userID int64, channelIDs []int64) (domain.DialogList, error) {
@ -63,7 +68,7 @@ func (s *Service) channelPeerDialogsReadModel(ctx context.Context, userID int64,
if len(unique) == 0 {
return domain.DialogList{}, nil
}
return s.cachedPeerDialogsReadModel(ctx, userID, unique, s.channelDialogHashes, s.loadChannelPeerDialogsByPeers)
return s.loadChannelPeerDialogsByPeers(ctx, userID, unique)
}
func (s *Service) cachedPeerDialogsReadModel(
@ -73,20 +78,20 @@ func (s *Service) cachedPeerDialogsReadModel(
hashesFor func(context.Context, int64, []domain.Peer) (map[domain.Peer]int64, error),
load func(context.Context, int64, []domain.Peer) (domain.DialogList, error),
) (domain.DialogList, error) {
if s.peerCache == nil || s.versions == nil {
if s.privatePeerCache == nil || s.versions == nil {
return load(ctx, userID, peers)
}
hashes, err := hashesFor(ctx, userID, peers)
if err != nil {
return domain.DialogList{}, err
}
loadEpoch := s.peerCache.cacheEpoch()
loadEpoch := s.privatePeerCache.cacheEpoch()
var out domain.DialogList
misses := make([]domain.Peer, 0, len(peers))
for _, peer := range peers {
hash := hashes[peer]
if hash != 0 {
if cached, ok := s.peerCache.lookup(dialogPeerCacheKey{userID: userID, peer: peer}, hash); ok {
if cached, ok := s.privatePeerCache.lookup(dialogPeerCacheKey{userID: userID, peer: peer}, hash); ok {
out = mergeDialogLists(out, cached)
continue
}
@ -108,7 +113,7 @@ func (s *Service) cachedPeerDialogsReadModel(
}
peerList := dialogListForPeer(list, peer)
peerList.Hash = hash
s.peerCache.putIfEpoch(dialogPeerCacheKey{userID: userID, peer: peer}, peerList, hash, loadEpoch)
s.privatePeerCache.putIfEpoch(dialogPeerCacheKey{userID: userID, peer: peer}, peerList, hash, loadEpoch)
}
if len(out.Dialogs) > 0 || len(out.Messages) > 0 || len(out.ChannelMessages) > 0 || len(out.Users) > 0 || len(out.Channels) > 0 {
return mergeDialogLists(out, list), nil
@ -124,11 +129,8 @@ func (s *Service) loadUserPeerDialogs(ctx context.Context, userID int64, peers [
if err != nil {
return domain.DialogList{}, err
}
if err := s.attachDrafts(ctx, userID, &list); err != nil {
return domain.DialogList{}, err
}
if err := s.projectDialogUsers(ctx, userID, &list); err != nil {
return domain.DialogList{}, err
for i := range list.Dialogs {
list.Dialogs[i].Draft = nil
}
return list, nil
}
@ -150,16 +152,10 @@ func (s *Service) loadChannelPeerDialogsByPeers(ctx context.Context, userID int6
if err != nil {
return domain.DialogList{}, err
}
if err := s.attachDrafts(ctx, userID, &out); err != nil {
return domain.DialogList{}, err
}
if err := s.projectDialogUsers(ctx, userID, &out); err != nil {
return domain.DialogList{}, err
}
return out, nil
}
func (s *Service) userDialogHashes(ctx context.Context, userID int64, peers []domain.Peer) (map[domain.Peer]int64, error) {
func (s *Service) dialogHashes(ctx context.Context, userID int64, peers []domain.Peer) (map[domain.Peer]int64, error) {
keys := make([]store.ReadModelKey, 0, len(peers))
for _, peer := range peers {
keys = append(keys, store.ReadModelKey{Model: dialogLightReadModel, OwnerUserID: userID, PeerType: peer.Type, PeerID: peer.ID})
@ -175,32 +171,6 @@ func (s *Service) userDialogHashes(ctx context.Context, userID int64, peers []do
return out, nil
}
func (s *Service) channelDialogHashes(ctx context.Context, userID int64, peers []domain.Peer) (map[domain.Peer]int64, error) {
keys := make([]store.ReadModelKey, 0, len(peers)*3)
for _, peer := range peers {
keys = append(keys,
store.ReadModelKey{Model: channelBaseReadModel, OwnerUserID: 0, PeerType: peer.Type, PeerID: peer.ID},
store.ReadModelKey{Model: channelMemberReadModel, OwnerUserID: userID, PeerType: peer.Type, PeerID: peer.ID},
store.ReadModelKey{Model: dialogLightReadModel, OwnerUserID: userID, PeerType: peer.Type, PeerID: peer.ID},
)
}
rows, err := s.versions.ReadModelHashes(ctx, keys)
if err != nil {
return nil, err
}
out := make(map[domain.Peer]int64, len(peers))
for _, peer := range peers {
base := rows[store.ReadModelKey{Model: channelBaseReadModel, OwnerUserID: 0, PeerType: peer.Type, PeerID: peer.ID}]
if base == 0 {
continue
}
member := rows[store.ReadModelKey{Model: channelMemberReadModel, OwnerUserID: userID, PeerType: peer.Type, PeerID: peer.ID}]
dialog := rows[store.ReadModelKey{Model: dialogLightReadModel, OwnerUserID: userID, PeerType: peer.Type, PeerID: peer.ID}]
out[peer] = readmodel.MixHashes(base, member, dialog)
}
return out, nil
}
// lookup 命中且版本(值自带 DialogList.Hash)匹配才返回;原语已在返回边界 clone。
func (c *dialogPeerReadModelCache) lookup(key dialogPeerCacheKey, currentHash int64) (domain.DialogList, bool) {
if c == nil {
@ -246,23 +216,79 @@ func (s *Service) InvalidateDialog(userID int64, peer domain.Peer) {
if s == nil || userID == 0 {
return
}
s.invalidateDialogListHashes(userID)
if s.peerCache == nil || peer.Type == "" || peer.ID == 0 {
s.InvalidateDialogOwner(userID)
if s.draftCache != nil && peer.Type != "" && peer.ID != 0 {
s.draftCache.invalidate(dialogPeerCacheKey{userID: userID, peer: peer})
}
if s.privatePeerCache != nil && peer.Type == domain.PeerTypeUser && peer.ID != 0 {
s.privatePeerCache.invalidate(dialogPeerCacheKey{userID: userID, peer: peer})
}
}
// InvalidateDialogOwner invalidates owner-list L1 state without inventing an
// exact peer. Redis L2 values are version-addressed and validated, so old keys
// expire naturally rather than requiring a global/key-pattern delete.
func (s *Service) InvalidateDialogOwner(userID int64) {
if s == nil || userID == 0 {
return
}
s.peerCache.invalidate(dialogPeerCacheKey{userID: userID, peer: peer})
s.invalidateDialogListHashes(userID)
if s.listCache != nil {
s.listCache.invalidateOwner(userID)
}
}
// InvalidateDialogListsForChannel invalidates only local bounded owner
// snapshots that actually contain the changed shared channel. The listener
// invokes it for channel_base; reconnect flush remains the missed-NOTIFY guard.
func (s *Service) InvalidateDialogListsForChannel(channelID int64) {
if s != nil && s.listCache != nil {
s.listCache.invalidateChannel(channelID)
}
}
func (s *Service) FlushReadModelCache() {
if s == nil {
return
}
if s.peerCache != nil {
s.peerCache.flush()
if s.privatePeerCache != nil {
s.privatePeerCache.flush()
}
if s.draftCache != nil {
s.draftCache.flush()
}
if s.listHashCache != nil {
s.listHashCache.flush()
}
if s.listCache != nil {
s.listCache.flush()
}
}
func dialogPeerListApproxBytes(list domain.DialogList) int64 {
weight := int64(256 + len(list.Dialogs)*256 + len(list.Messages)*512 + len(list.Users)*512)
for _, dialog := range list.Dialogs {
weight += int64(len(dialog.ThemeEmoticon))
}
for _, msg := range list.Messages {
weight += int64(len(msg.Body) + len(msg.Entities)*64)
if msg.ReplyTo != nil {
weight += int64(128 + len(msg.ReplyTo.QuoteText) + len(msg.ReplyTo.QuoteEntities)*64)
}
if msg.Forward != nil {
weight += int64(96 + len(msg.Forward.FromName))
}
if msg.RichMessage != nil {
weight += int64(len(msg.RichMessage.Blocks) + len(msg.RichMessage.BotAPIProjection) + len(msg.RichMessage.Photos)*256 + len(msg.RichMessage.Documents)*256)
}
}
for _, user := range list.Users {
weight += int64(len(user.Phone) + len(user.FirstName) + len(user.LastName) + len(user.About) + len(user.Username) + len(user.PhotoStripped))
}
if weight < 1 {
return 1
}
return weight
}
func (s *Service) invalidateDialogListHashes(userID int64) {
@ -373,9 +399,22 @@ func cloneDialogList(in domain.DialogList) domain.DialogList {
in.ChannelMessages = cloneDialogChannelMessages(in.ChannelMessages)
in.Users = cloneDialogUsers(in.Users)
in.Channels = cloneDialogChannels(in.Channels)
in.ArchiveSummary = cloneDialogArchiveSummary(in.ArchiveSummary)
return in
}
func cloneDialogArchiveSummary(in *domain.DialogArchiveSummary) *domain.DialogArchiveSummary {
if in == nil {
return nil
}
out := *in
if in.TopDialog != nil {
dialog := cloneDialog(*in.TopDialog)
out.TopDialog = &dialog
}
return &out
}
func cloneDialogSlice(in []domain.Dialog) []domain.Dialog {
out := make([]domain.Dialog, len(in))
for i := range in {
@ -385,6 +424,14 @@ func cloneDialogSlice(in []domain.Dialog) []domain.Dialog {
}
func cloneDialog(in domain.Dialog) domain.Dialog {
if in.DefaultSendAs != nil {
peer := *in.DefaultSendAs
in.DefaultSendAs = &peer
}
if in.ChannelMember != nil {
member := *in.ChannelMember
in.ChannelMember = &member
}
if in.Draft != nil {
draft := cloneDraft(*in.Draft)
in.Draft = &draft