merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
|
|
@ -2,13 +2,56 @@ package rpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/app/peerview"
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
maxPeerProjectionUsersPerBatch = 1000
|
||||
maxPeerProjectionRecoveryCalls = 64
|
||||
maxPeerProjectionAttemptedOwnerIDs = 16000
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDurableUserProjectionIncomplete = errors.New("durable user projection is incomplete")
|
||||
ErrUserProjectionCapacityRecoveryLimit = errors.New("user projection capacity recovery limit exceeded")
|
||||
)
|
||||
|
||||
type userProjectionRecoveryBudget struct {
|
||||
maxCalls int
|
||||
maxItems int
|
||||
calls int
|
||||
attempted int
|
||||
}
|
||||
|
||||
func (b *userProjectionRecoveryBudget) consume(items int) error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
itemsExceeded := items < 0 || b.attempted > b.maxItems || items > b.maxItems-b.attempted
|
||||
if b.calls >= b.maxCalls || itemsExceeded {
|
||||
return fmt.Errorf(
|
||||
"%w: calls=%d/%d attempted=%d/%d next=%d",
|
||||
ErrUserProjectionCapacityRecoveryLimit,
|
||||
b.calls,
|
||||
b.maxCalls,
|
||||
b.attempted,
|
||||
b.maxItems,
|
||||
items,
|
||||
)
|
||||
}
|
||||
b.calls++
|
||||
b.attempted += items
|
||||
return nil
|
||||
}
|
||||
|
||||
// viewerPeerCache 是一次 outbox/fanout 构建内的短生命周期缓存。
|
||||
// 用户资料含联系人、隐私、头像和在线状态,必须按 viewerUserID 隔离,不能跨视角复用。
|
||||
type viewerPeerCache struct {
|
||||
|
|
@ -33,21 +76,77 @@ func newViewerPeerCache(r *Router) *viewerPeerCache {
|
|||
}
|
||||
|
||||
func (c *viewerPeerCache) usersForIDs(ctx context.Context, viewerUserID int64, ids []int64) []domain.User {
|
||||
if c == nil || c.users == nil {
|
||||
return nil
|
||||
}
|
||||
users, err := c.users.UsersForView(ctx, viewerUserID, ids)
|
||||
if err != nil && c.r != nil {
|
||||
unique := uniquePeerIDs(ids)
|
||||
users, err := c.resolveUsersForIDs(ctx, viewerUserID, unique)
|
||||
if err != nil && c != nil && c.r != nil {
|
||||
c.r.log.Warn("batch resolve users for peer projection failed",
|
||||
zap.Int64("viewer_user_id", viewerUserID),
|
||||
zap.Int("count", len(uniquePeerIDs(ids))),
|
||||
zap.Int("count", len(unique)),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
if c.r == nil {
|
||||
return users
|
||||
return users
|
||||
}
|
||||
|
||||
// usersForIDsStrict rejects resolver errors and incomplete durable envelopes.
|
||||
// Callers must not merge a partial projection with raw store users while
|
||||
// advancing an account/channel difference cursor or dispatching an outbox row.
|
||||
func (c *viewerPeerCache) usersForIDsStrict(ctx context.Context, viewerUserID int64, ids []int64) ([]domain.User, error) {
|
||||
unique := uniquePeerIDs(ids)
|
||||
users, err := c.resolveUsersForIDs(ctx, viewerUserID, unique)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve durable users for viewer %d: %w", viewerUserID, err)
|
||||
}
|
||||
return c.r.withUsersPresence(users)
|
||||
if missingID, missing := missingProjectedUserID(unique, users); missing {
|
||||
return nil, fmt.Errorf("%w: viewer_user_id=%d missing_user_id=%d", ErrDurableUserProjectionIncomplete, viewerUserID, missingID)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (c *viewerPeerCache) resolveUsersForIDs(ctx context.Context, viewerUserID int64, unique []int64) ([]domain.User, error) {
|
||||
if c == nil || c.users == nil {
|
||||
return nil, nil
|
||||
}
|
||||
budget := userProjectionRecoveryBudget{
|
||||
maxCalls: maxPeerProjectionRecoveryCalls,
|
||||
maxItems: maxPeerProjectionAttemptedOwnerIDs,
|
||||
}
|
||||
users := make([]domain.User, 0, len(unique))
|
||||
for start := 0; start < len(unique); start += maxPeerProjectionUsersPerBatch {
|
||||
end := start + maxPeerProjectionUsersPerBatch
|
||||
if end > len(unique) {
|
||||
end = len(unique)
|
||||
}
|
||||
batch, err := c.usersForIDBatch(ctx, viewerUserID, unique[start:end], &budget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, batch...)
|
||||
}
|
||||
if c.r == nil {
|
||||
return users, nil
|
||||
}
|
||||
return c.r.withUsersPresence(users), nil
|
||||
}
|
||||
|
||||
func (c *viewerPeerCache) usersForIDBatch(ctx context.Context, viewerUserID int64, ids []int64, budget *userProjectionRecoveryBudget) ([]domain.User, error) {
|
||||
if err := budget.consume(len(ids)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users, err := c.users.UsersForView(ctx, viewerUserID, ids)
|
||||
if err == nil || !isSparseProjectionCapacityError(err) || len(ids) < 2 {
|
||||
return users, err
|
||||
}
|
||||
middle := len(ids) / 2
|
||||
left, leftErr := c.usersForIDBatch(ctx, viewerUserID, ids[:middle], budget)
|
||||
if leftErr != nil {
|
||||
return nil, leftErr
|
||||
}
|
||||
right, rightErr := c.usersForIDBatch(ctx, viewerUserID, ids[middle:], budget)
|
||||
if rightErr != nil {
|
||||
return nil, rightErr
|
||||
}
|
||||
return append(left, right...), nil
|
||||
}
|
||||
|
||||
// primeUsers 把跨 viewer 一次性投影(ByIDsForViewers)的结果按 viewer 预热进底层 BatchCache,
|
||||
|
|
@ -59,6 +158,40 @@ func (c *viewerPeerCache) primeUsers(viewerUserID int64, users []domain.User) {
|
|||
c.users.Prime(viewerUserID, users)
|
||||
}
|
||||
|
||||
// primeExpectedUsers follows a caller-side completeness check. PrimeExpected
|
||||
// also negative-caches omitted system-resolved IDs so later lookups cannot
|
||||
// silently fall back to a scalar resolver.
|
||||
func (c *viewerPeerCache) primeExpectedUsers(viewerUserID int64, expectedIDs []int64, users []domain.User) {
|
||||
if c == nil || c.users == nil || viewerUserID == 0 {
|
||||
return
|
||||
}
|
||||
c.users.PrimeExpected(viewerUserID, expectedIDs, users)
|
||||
}
|
||||
|
||||
func missingProjectedUserID(expectedIDs []int64, users []domain.User) (int64, bool) {
|
||||
found := make(map[int64]struct{}, len(users))
|
||||
for _, user := range users {
|
||||
if user.ID != 0 {
|
||||
found[user.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, id := range uniquePeerIDs(expectedIDs) {
|
||||
if _, system := domain.SystemUserByID(id); system {
|
||||
continue
|
||||
}
|
||||
if _, ok := found[id]; !ok {
|
||||
return id, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func isSparseProjectionCapacityError(err error) bool {
|
||||
return errors.Is(err, store.ErrActiveChannelMemberPairsLimit) ||
|
||||
errors.Is(err, appusers.ErrBatchUsersLimit) ||
|
||||
errors.Is(err, appusers.ErrBatchViewerCells)
|
||||
}
|
||||
|
||||
func (c *viewerPeerCache) channelsForIDs(ctx context.Context, viewerUserID int64, ids []int64) []domain.Channel {
|
||||
unique := uniquePeerIDs(ids)
|
||||
if c == nil || c.r == nil || len(unique) == 0 || c.r.deps.Channels == nil || viewerUserID == 0 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue