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

@ -3,6 +3,7 @@ package rpc
import (
"context"
"errors"
"fmt"
"sort"
"sync"
"time"
@ -16,7 +17,7 @@ import (
)
const (
defaultOutboxBatch = 100
defaultOutboxBatch = 10
defaultOutboxInterval = 200 * time.Millisecond
defaultOutboxWorkers = 4
// outboxLogicalShards 是稳定 user→lane 哈希空间。它不随运行时 worker 数变化,
@ -31,6 +32,9 @@ const (
var (
errMissingOutboxEvent = errors.New("missing outbox update event")
errOutboxUpdateBuilderMissing = errors.New("outbox update builder is required")
errOutboxUpdateBuilderCount = errors.New("outbox update builder returned mismatched count")
errOutboxUpdateBuilderEmpty = errors.New("outbox update builder returned a nil non-noop update")
errInvalidOutboxExclusionPair = errors.New("outbox exclusion requires both raw auth key and session id")
)
@ -60,7 +64,7 @@ type OutboxUpdateRequest struct {
}
// OutboxUpdateBuilder 按接收者视角批量把 domain.UpdateEvent 转为 TL updates。
type OutboxUpdateBuilder func(ctx context.Context, requests []OutboxUpdateRequest) []*tg.Updates
type OutboxUpdateBuilder func(ctx context.Context, requests []OutboxUpdateRequest) ([]*tg.Updates, error)
// WithOutboxUpdateBuilder 注入按接收者视角的批量 updates 构建器。
func WithOutboxUpdateBuilder(builder OutboxUpdateBuilder) OutboxOption {
@ -333,7 +337,24 @@ func (d *OutboxDispatcher) dispatchBatch(ctx context.Context, items []store.Disp
ready = append(ready, outboxDispatchReady{item: item})
requests = append(requests, OutboxUpdateRequest{TargetUserID: item.TargetUserID, Event: event})
}
builtUpdates := d.buildOutboxUpdates(ctx, requests)
builtUpdates, err := d.buildOutboxUpdates(ctx, requests)
if err != nil {
// 聚合构建失败不代表每个 durable row 都坏了。复用本批已经加载的 event
// 做 singleton 隔离,避免某一条坏数据或批量容量错误污染无关用户;同一用户
// 的 lane head 一旦失败,后续 pts 仍不得越过。
d.log.Warn("batch build dispatch outbox updates; isolating items", zap.Error(err))
clear(blockedUsers)
for i, entry := range ready {
item := entry.item
if _, blocked := blockedUsers[item.TargetUserID]; blocked {
continue
}
if !d.dispatchPreparedItem(ctx, item, requests[i].Event, time.Now()) {
blockedUsers[item.TargetUserID] = struct{}{}
}
}
return
}
delivered := make([]store.DispatchOutboxItem, 0, len(items))
clear(blockedUsers)
for i, entry := range ready {
@ -395,7 +416,17 @@ func (d *OutboxDispatcher) dispatchItem(ctx context.Context, item store.Dispatch
d.markDispatchFailed(ctx, item, errMissingOutboxEvent)
return false
}
update := d.buildOutboxUpdate(ctx, item, events[0])
return d.dispatchPreparedItem(ctx, item, events[0], start)
}
// dispatchPreparedItem 构建并投递一条已经加载、且 exclusion pair 已验证的 event。
// 批量构建隔离路径调用它时不会再次读取 event store。
func (d *OutboxDispatcher) dispatchPreparedItem(ctx context.Context, item store.DispatchOutboxItem, event domain.UpdateEvent, start time.Time) bool {
update, err := d.buildOutboxUpdate(ctx, item, event)
if err != nil {
d.markDispatchFailed(ctx, item, err)
return false
}
if update == nil {
if err := d.outbox.MarkDelivered(ctx, item); err != nil {
d.log.Warn("mark noop dispatch delivered", zap.Int64("target_user_id", item.TargetUserID), zap.Int64("outbox_id", item.ID), zap.Error(err))
@ -433,33 +464,38 @@ func (d *OutboxDispatcher) dispatchItem(ctx context.Context, item store.Dispatch
return true
}
func (d *OutboxDispatcher) buildOutboxUpdate(ctx context.Context, item store.DispatchOutboxItem, event domain.UpdateEvent) *tg.Updates {
updates := d.buildOutboxUpdates(ctx, []OutboxUpdateRequest{{TargetUserID: item.TargetUserID, Event: event}})
if len(updates) == 0 {
return nil
func (d *OutboxDispatcher) buildOutboxUpdate(ctx context.Context, item store.DispatchOutboxItem, event domain.UpdateEvent) (*tg.Updates, error) {
updates, err := d.buildOutboxUpdates(ctx, []OutboxUpdateRequest{{TargetUserID: item.TargetUserID, Event: event}})
if err != nil {
return nil, err
}
return updates[0]
if len(updates) == 0 {
return nil, nil
}
return updates[0], nil
}
func (d *OutboxDispatcher) buildOutboxUpdates(ctx context.Context, requests []OutboxUpdateRequest) []*tg.Updates {
func (d *OutboxDispatcher) buildOutboxUpdates(ctx context.Context, requests []OutboxUpdateRequest) ([]*tg.Updates, error) {
out := make([]*tg.Updates, len(requests))
if len(requests) == 0 {
return out
return out, nil
}
if d.updateBuilder != nil {
built := d.updateBuilder(ctx, requests)
if len(built) == len(requests) {
return built
if d.updateBuilder == nil {
return nil, errOutboxUpdateBuilderMissing
}
built, err := d.updateBuilder(ctx, requests)
if err != nil {
return nil, err
}
if len(built) != len(requests) {
return nil, fmt.Errorf("%w: got %d want %d", errOutboxUpdateBuilderCount, len(built), len(requests))
}
for i := range built {
if built[i] == nil && requests[i].Event.Type != domain.UpdateEventNoop {
return nil, fmt.Errorf("%w: index=%d user_id=%d pts=%d event_type=%s", errOutboxUpdateBuilderEmpty, i, requests[i].TargetUserID, requests[i].Event.Pts, requests[i].Event.Type)
}
d.log.Warn("outbox update builder returned mismatched count",
zap.Int("requests", len(requests)),
zap.Int("updates", len(built)),
)
}
for i, req := range requests {
out[i] = tgUpdateForOutboxEvent(req.Event)
}
return out
return built, nil
}
// pushOutboxUpdate 投递一条 outbox update,返回 (送达的在线 session 数, 是否可重试, err)。
@ -527,6 +563,9 @@ func tgUpdateForOutboxEventForViewer(event domain.UpdateEvent, viewerUserID int6
}
switch event.Type {
case domain.UpdateEventNewMessage:
if event.Message.Deleted {
return tgDeletedPrivateMessageOutboxUpdate(event)
}
return tgPrivateMessageUpdates(event, event.Message, 0, false, tgUsersForViewer(viewerUserID, event.Users), tgChannels(viewerUserID, event.Channels))
case domain.UpdateEventReadHistoryInbox, domain.UpdateEventReadHistoryOutbox:
var update tg.UpdateClass
@ -569,6 +608,29 @@ func tgUpdateForOutboxEventForViewer(event domain.UpdateEvent, viewerUserID int6
}
}
func tgDeletedPrivateMessageOutboxUpdate(event domain.UpdateEvent) *tg.Updates {
if event.Pts <= 0 || event.PtsCount <= 0 {
return nil
}
ids := []int{}
if event.Message.ID > 0 && event.Message.ID <= domain.MaxMessageBoxID {
ids = append(ids, event.Message.ID)
}
date := event.Date
if date == 0 {
date = event.Message.Date
}
return &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateDeleteMessages{
Messages: ids,
Pts: event.Pts,
PtsCount: event.PtsCount,
}},
Date: date,
Seq: 0,
}
}
// appendAuxPtsBookkeeping 给"占账号 pts 但 TL update 不带 pts"的事件附一条
// 空 updateDeleteMessages:客户端按 pts/pts_count 推进本地水位且不产生任何
// 可见变化。没有它,客户端水位停在事件前,下一条带 pts 的更新会被判为空洞。