fix: sync moderation refresh cache bypass

This commit is contained in:
iamxvbaba 2026-07-24 11:57:00 +08:00
parent 3dd9c345d7
commit 6cafa40c7b
6 changed files with 187 additions and 0 deletions

View file

@ -249,6 +249,15 @@ type UsersService interface {
ByIDs(ctx context.Context, currentUserID int64, userIDs []int64) ([]domain.User, error)
}
// UserAuthoritativeProjectionService bypasses viewer-independent base caches
// for an explicit durable profile-refresh event. The event exists precisely
// because a just-committed absolute user fact must replace client and server
// caches; re-reading a stale Redis value would acknowledge the outbox row
// without ever exposing the committed state.
type UserAuthoritativeProjectionService interface {
ByIDsAuthoritative(ctx context.Context, currentUserID int64, userIDs []int64) ([]domain.User, error)
}
// TelegramLoginService is the domain-only boundary shared by the MTProto RPC
// edge and the public OIDC provider. PostgreSQL remains authoritative for all
// consent transitions; the RPC layer only projects domain state to TL.
@ -781,6 +790,12 @@ type ChannelsService interface {
FilterActiveMemberIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error)
}
// ChannelAuthoritativeProjectionService bypasses long-lived channel read
// models for a durable channel_state refresh emitted by an admin mutation.
type ChannelAuthoritativeProjectionService interface {
GetChannelsAuthoritative(ctx context.Context, userID int64, channelIDs []int64) ([]domain.ChannelView, error)
}
// CommunitiesService abstracts the Layer 228 Community aggregation domain.
// Community containers never expose tg types and never own message/read/pts state.
type CommunitiesService interface {

View file

@ -1,13 +1,47 @@
package rpc
import (
"context"
"testing"
"github.com/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/tg"
"go.uber.org/zap"
"telesrv/internal/domain"
)
type moderationProjectionUsers struct {
UsersService
freshCalls int
}
func (s *moderationProjectionUsers) ByIDs(_ context.Context, _ int64, ids []int64) ([]domain.User, error) {
return []domain.User{{ID: ids[0], FirstName: "stale"}}, nil
}
func (s *moderationProjectionUsers) ByIDsAuthoritative(_ context.Context, _ int64, ids []int64) ([]domain.User, error) {
s.freshCalls++
return []domain.User{{ID: ids[0], FirstName: "fresh", Scam: true}}, nil
}
type moderationProjectionChannels struct {
ChannelsService
freshCalls int
}
func (s *moderationProjectionChannels) GetChannels(_ context.Context, _ int64, ids []int64) ([]domain.ChannelView, error) {
return []domain.ChannelView{{Channel: domain.Channel{ID: ids[0], Title: "stale", Megagroup: true}}}, nil
}
func (s *moderationProjectionChannels) GetChannelsAuthoritative(_ context.Context, viewerUserID int64, ids []int64) ([]domain.ChannelView, error) {
s.freshCalls++
return []domain.ChannelView{{
Channel: domain.Channel{ID: ids[0], Title: "fresh", Megagroup: true, Scam: true},
Self: domain.ChannelMember{ChannelID: ids[0], UserID: viewerUserID, Status: domain.ChannelMemberActive},
}}, nil
}
func TestModerationProfileUpdateCarriesStandardFlagsAndPts(t *testing.T) {
const (
viewerID = int64(1001)
@ -95,3 +129,28 @@ func TestChannelModerationUpdateCarriesStandardFlagsAndPts(t *testing.T) {
t.Fatalf("channel = %T %+v", updates.Chats[0], updates.Chats[0])
}
}
func TestModerationRefreshEventsBypassServerProjectionCaches(t *testing.T) {
users := &moderationProjectionUsers{}
channels := &moderationProjectionChannels{}
r := New(Config{}, Deps{Users: users, Channels: channels}, zap.NewNop(), clock.System)
const viewerID = int64(5005)
events := r.enrichUpdateEvents(context.Background(), viewerID, []domain.UpdateEvent{
{
UserID: viewerID, Type: domain.UpdateEventUserProfile,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 6006},
},
{
UserID: viewerID, Type: domain.UpdateEventChannelState,
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 7007},
},
})
if users.freshCalls != 1 || len(events[0].Users) != 1 ||
events[0].Users[0].FirstName != "fresh" || !events[0].Users[0].Scam {
t.Fatalf("authoritative user refresh = calls:%d users:%+v", users.freshCalls, events[0].Users)
}
if channels.freshCalls != 1 || len(events[1].Channels) != 1 ||
events[1].Channels[0].Title != "fresh" || !events[1].Channels[0].Scam {
t.Fatalf("authoritative channel refresh = calls:%d channels:%+v", channels.freshCalls, events[1].Channels)
}
}

View file

@ -3,6 +3,8 @@ package rpc
import (
"context"
"go.uber.org/zap"
"telesrv/internal/domain"
)
@ -25,6 +27,38 @@ func (r *Router) enrichUpdateEventsWithPeerCache(ctx context.Context, viewerUser
allUserIDs := make(map[int64]struct{})
allChannelIDs := make(map[int64]struct{})
for i := range out {
if out[i].Type == domain.UpdateEventUserProfile {
if service, ok := r.deps.Users.(UserAuthoritativeProjectionService); ok {
users, err := service.ByIDsAuthoritative(ctx, viewerUserID, []int64{out[i].Peer.ID})
if err != nil {
r.log.Warn("reload authoritative user profile event",
zap.Int64("viewer_user_id", viewerUserID),
zap.Int64("target_user_id", out[i].Peer.ID),
zap.Error(err))
} else {
out[i].Users = users
cache.primeUsers(viewerUserID, users)
}
}
}
if out[i].Type == domain.UpdateEventChannelState {
if service, ok := r.deps.Channels.(ChannelAuthoritativeProjectionService); ok {
views, err := service.GetChannelsAuthoritative(ctx, viewerUserID, []int64{out[i].Peer.ID})
if err != nil {
r.log.Warn("reload authoritative channel state event",
zap.Int64("viewer_user_id", viewerUserID),
zap.Int64("channel_id", out[i].Peer.ID),
zap.Error(err))
} else {
out[i].Channels = out[i].Channels[:0]
for _, view := range views {
if view.Channel.ID != 0 {
out[i].Channels = append(out[i].Channels, view.Channel)
}
}
}
}
}
if out[i].Type == domain.UpdateEventMessageReactions {
out[i] = r.enrichMessageReactionEvent(ctx, viewerUserID, out[i])
}