Initial open source release

This commit is contained in:
A 2026-06-04 01:37:39 +08:00
commit 74992e893f
377 changed files with 118084 additions and 0 deletions

View file

@ -0,0 +1,458 @@
package rpc
import (
"context"
"errors"
"testing"
"time"
"github.com/gotd/td/bin"
"github.com/gotd/td/proto"
"github.com/gotd/td/tg"
"go.uber.org/zap/zaptest"
"telesrv/internal/domain"
"telesrv/internal/store"
)
func TestOutboxDispatcherPushesNewMessageAndMarksDelivered(t *testing.T) {
msg := domain.Message{
ID: 10,
OwnerUserID: 1000000002,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001},
From: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001},
Date: 1700000300,
Body: "hello",
Pts: 7,
}
outbox := &captureDispatchOutbox{items: []store.DispatchOutboxItem{{
ID: 55,
TargetUserID: msg.OwnerUserID,
Pts: msg.Pts,
EventType: domain.UpdateEventNewMessage,
ExcludeSessionID: 99,
}}}
events := &captureUpdateEventStore{events: []domain.UpdateEvent{{
UserID: msg.OwnerUserID,
Type: domain.UpdateEventNewMessage,
Pts: msg.Pts,
PtsCount: 1,
Date: msg.Date,
Message: msg,
Users: []domain.User{{
ID: msg.From.ID,
FirstName: "Sender",
}},
}}}
sessions := &captureSessions{}
metrics := &captureOutboxMetrics{}
dispatcher := NewOutboxDispatcher(events, outbox, sessions, zaptest.NewLogger(t), WithOutboxMetrics(metrics))
dispatcher.DispatchOnce(context.Background())
if !outbox.delivered || outbox.deliveredUserID != msg.OwnerUserID || outbox.deliveredID != 55 {
t.Fatalf("delivered = %v user=%d id=%d, want outbox delivered", outbox.delivered, outbox.deliveredUserID, outbox.deliveredID)
}
if sessions.userID != msg.OwnerUserID || sessions.sessionID != 99 || sessions.messageType != proto.MessageFromServer {
t.Fatalf("push target = user %d exclude %d type %v, want outbox target/exclude", sessions.userID, sessions.sessionID, sessions.messageType)
}
updates, ok := sessions.message.(*tg.Updates)
if !ok {
t.Fatalf("pushed message = %T, want *tg.Updates", sessions.message)
}
if len(updates.Updates) != 1 || len(updates.Users) != 1 {
t.Fatalf("updates = %+v, want one update and sender user", updates)
}
update, ok := updates.Updates[0].(*tg.UpdateNewMessage)
if !ok || update.Pts != msg.Pts {
t.Fatalf("update = %#v, want UpdateNewMessage pts=%d", updates.Updates[0], msg.Pts)
}
if metrics.claimed != 1 || metrics.delivered != 1 || metrics.failed != 0 {
t.Fatalf("metrics = claimed %d delivered %d failed %d, want 1/1/0", metrics.claimed, metrics.delivered, metrics.failed)
}
}
func TestOutboxDispatcherUsesScopedAuthKeyExclusion(t *testing.T) {
var excludeAuthKeyID [8]byte
excludeAuthKeyID[0] = 7
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001}
outbox := &captureDispatchOutbox{items: []store.DispatchOutboxItem{{
ID: 57,
TargetUserID: 1000000002,
Pts: 9,
EventType: domain.UpdateEventPeerSettings,
ExcludeAuthKeyID: excludeAuthKeyID,
ExcludeSessionID: 99,
}}}
events := &captureUpdateEventStore{events: []domain.UpdateEvent{{
UserID: 1000000002,
Type: domain.UpdateEventPeerSettings,
Pts: 9,
PtsCount: 1,
Date: 1700000302,
Peer: peer,
}}}
sessions := &captureScopedSessions{captureSessions: &captureSessions{}}
dispatcher := NewOutboxDispatcher(events, outbox, sessions, zaptest.NewLogger(t))
dispatcher.DispatchOnce(context.Background())
if sessions.scopedAuthKeyID != excludeAuthKeyID || sessions.sessionID != 99 || sessions.userID != 1000000002 {
t.Fatalf("scoped push = auth %x session %d user %d, want precise outbox exclusion", sessions.scopedAuthKeyID, sessions.sessionID, sessions.userID)
}
}
// TestOutboxDispatcherBatchPath 覆盖生产批量路径:store 同时具备 BatchByCursor + MarkDeliveredBatch
// 时,DispatchOnce 一次批量取事件、推送、再批量标记 delivered,而非逐条。
func TestOutboxDispatcherBatchPath(t *testing.T) {
msg := domain.Message{
ID: 10,
OwnerUserID: 1000000002,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001},
From: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001},
Date: 1700000300,
Body: "hello",
Pts: 7,
}
events := &batchEventStore{captureUpdateEventStore: &captureUpdateEventStore{events: []domain.UpdateEvent{{
UserID: msg.OwnerUserID,
Type: domain.UpdateEventNewMessage,
Pts: msg.Pts,
PtsCount: 1,
Date: msg.Date,
Message: msg,
Users: []domain.User{{ID: msg.From.ID, FirstName: "Sender"}},
}}}}
outbox := &batchDispatchOutbox{captureDispatchOutbox: &captureDispatchOutbox{items: []store.DispatchOutboxItem{{
ID: 55,
TargetUserID: msg.OwnerUserID,
Pts: msg.Pts,
EventType: domain.UpdateEventNewMessage,
ExcludeSessionID: 99,
}}}}
sessions := &captureSessions{}
metrics := &captureOutboxMetrics{}
dispatcher := NewOutboxDispatcher(events, outbox, sessions, zaptest.NewLogger(t), WithOutboxMetrics(metrics))
dispatcher.DispatchOnce(context.Background())
if len(events.batchCursors) != 1 || events.batchCursors[0] != (store.EventCursor{UserID: msg.OwnerUserID, Pts: msg.Pts}) {
t.Fatalf("batch cursors = %+v, want one cursor for (%d,%d)", events.batchCursors, msg.OwnerUserID, msg.Pts)
}
if sessions.userID != msg.OwnerUserID || sessions.sessionID != 99 {
t.Fatalf("push target = user %d exclude %d, want batch push to outbox target", sessions.userID, sessions.sessionID)
}
if len(outbox.deliveredBatch) != 1 || outbox.deliveredBatch[0].ID != 55 {
t.Fatalf("delivered batch = %+v, want one item id=55", outbox.deliveredBatch)
}
if outbox.delivered {
t.Fatalf("batch path should not call per-item MarkDelivered")
}
if metrics.claimed != 1 || metrics.delivered != 1 || metrics.failed != 0 {
t.Fatalf("metrics = claimed %d delivered %d failed %d, want 1/1/0", metrics.claimed, metrics.delivered, metrics.failed)
}
}
func TestOutboxDispatcherUsesBestEffortPush(t *testing.T) {
msg := domain.Message{
ID: 10,
OwnerUserID: 1000000002,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001},
From: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001},
Date: 1700000300,
Body: "hello",
Pts: 7,
}
events := &captureUpdateEventStore{events: []domain.UpdateEvent{{
UserID: msg.OwnerUserID,
Type: domain.UpdateEventNewMessage,
Pts: msg.Pts,
PtsCount: 1,
Date: msg.Date,
Message: msg,
Users: []domain.User{{ID: msg.From.ID, FirstName: "Sender"}},
}}}
outbox := &captureDispatchOutbox{items: []store.DispatchOutboxItem{{
ID: 55,
TargetUserID: msg.OwnerUserID,
Pts: msg.Pts,
EventType: domain.UpdateEventNewMessage,
ExcludeSessionID: 99,
}}}
sessions := &captureBestEffortSessions{captureSessions: &captureSessions{}}
dispatcher := NewOutboxDispatcher(events, outbox, sessions, zaptest.NewLogger(t), WithOutboxPushTimeout(50*time.Millisecond))
dispatcher.DispatchOnce(context.Background())
if !sessions.bestEffort || sessions.timeout != 50*time.Millisecond {
t.Fatalf("best-effort push = %v timeout %v, want true/50ms", sessions.bestEffort, sessions.timeout)
}
if !outbox.delivered || outbox.failed {
t.Fatalf("outbox delivered=%v failed=%v, want delivered after accepted best-effort push", outbox.delivered, outbox.failed)
}
}
type captureBestEffortSessions struct {
*captureSessions
bestEffort bool
timeout time.Duration
}
func (s *captureBestEffortSessions) PushToUserExceptSessionBestEffort(ctx context.Context, userID, excludeSessionID int64, t proto.MessageType, msg bin.Encoder, timeout time.Duration) (int, error) {
s.bestEffort = true
s.timeout = timeout
return s.PushToUserExceptSession(ctx, userID, excludeSessionID, t, msg)
}
// batchEventStore 给 captureUpdateEventStore 加上 BatchByCursor 批量能力。
type batchEventStore struct {
*captureUpdateEventStore
batchCursors []store.EventCursor
}
func (s *batchEventStore) BatchByCursor(_ context.Context, cursors []store.EventCursor) ([]domain.UpdateEvent, error) {
s.batchCursors = cursors
out := make([]domain.UpdateEvent, 0, len(cursors))
for _, c := range cursors {
for _, event := range s.events {
if event.UserID == c.UserID && event.Pts == c.Pts {
out = append(out, event)
}
}
}
return out, nil
}
// batchDispatchOutbox 给 captureDispatchOutbox 加上 MarkDeliveredBatch 批量能力。
type batchDispatchOutbox struct {
*captureDispatchOutbox
deliveredBatch []store.DispatchOutboxItem
}
func (s *batchDispatchOutbox) MarkDeliveredBatch(_ context.Context, items []store.DispatchOutboxItem) error {
s.deliveredBatch = append(s.deliveredBatch, items...)
return nil
}
type captureUpdateEventStore struct {
events []domain.UpdateEvent
}
func (s *captureUpdateEventStore) Append(context.Context, int64, domain.UpdateEvent) error {
return nil
}
func (s *captureUpdateEventStore) ListAfter(_ context.Context, _ int64, pts, limit int) ([]domain.UpdateEvent, error) {
out := make([]domain.UpdateEvent, 0, len(s.events))
for _, event := range s.events {
if event.Pts > pts {
out = append(out, event)
if len(out) == limit {
break
}
}
}
return out, nil
}
func (s *captureUpdateEventStore) Current(context.Context, int64) (int, error) {
maxPts := 0
for _, event := range s.events {
if event.Pts > maxPts {
maxPts = event.Pts
}
}
return maxPts, nil
}
func (s *captureUpdateEventStore) MaxContiguousPts(context.Context, int64) (int, error) {
present := make(map[int]struct{}, len(s.events))
for _, event := range s.events {
present[event.Pts] = struct{}{}
}
contiguous := 0
for {
if _, ok := present[contiguous+1]; !ok {
break
}
contiguous++
}
return contiguous, nil
}
func (s *captureUpdateEventStore) AdvanceContiguousPts(ctx context.Context, userID int64) (int, error) {
return s.MaxContiguousPts(ctx, userID)
}
type captureDispatchOutbox struct {
items []store.DispatchOutboxItem
delivered bool
deliveredUserID int64
deliveredID int64
failed bool
failedError string
}
type captureScopedSessions struct {
*captureSessions
scopedAuthKeyID [8]byte
}
func (s *captureScopedSessions) BindAuthKeyForSession(rawAuthKeyID [8]byte, sessionID int64, authKeyID [8]byte) {
s.BindAuthKey(sessionID, authKeyID)
s.scopedAuthKeyID = rawAuthKeyID
}
func (s *captureScopedSessions) AuthKeyIDForSession([8]byte, int64) ([8]byte, bool) {
return s.AuthKeyID(0)
}
func (s *captureScopedSessions) BindUserForAuthKey(rawAuthKeyID [8]byte, sessionID, userID int64) {
s.BindUser(sessionID, userID)
s.scopedAuthKeyID = rawAuthKeyID
}
func (s *captureScopedSessions) UserIDForAuthKey([8]byte, int64) (int64, bool) {
return s.UserID(0)
}
func (s *captureScopedSessions) UserIDResolvedForAuthKey([8]byte, int64) (int64, bool) {
return s.UserIDResolved(0)
}
func (s *captureScopedSessions) SetReceivesUpdatesForAuthKey([8]byte, int64, bool) {}
func (s *captureScopedSessions) PushToSessionForAuthKey(_ context.Context, rawAuthKeyID [8]byte, sessionID int64, t proto.MessageType, msg bin.Encoder) error {
s.scopedAuthKeyID = rawAuthKeyID
return s.PushToSession(context.Background(), sessionID, t, msg)
}
func (s *captureScopedSessions) PushToUserExceptAuthKeySession(_ context.Context, userID int64, excludeAuthKeyID [8]byte, excludeSessionID int64, t proto.MessageType, msg bin.Encoder) (int, error) {
s.scopedAuthKeyID = excludeAuthKeyID
return s.PushToUserExceptSession(context.Background(), userID, excludeSessionID, t, msg)
}
func (s *captureDispatchOutbox) ClaimPending(context.Context, int) ([]store.DispatchOutboxItem, error) {
items := s.items
s.items = nil
return items, nil
}
func (s *captureDispatchOutbox) MarkDelivered(_ context.Context, targetUserID, id int64) error {
s.delivered = true
s.deliveredUserID = targetUserID
s.deliveredID = id
return nil
}
func (s *captureDispatchOutbox) MarkFailed(_ context.Context, _ int64, _ int64, lastError string) error {
s.failed = true
s.failedError = lastError
return nil
}
func (s *captureDispatchOutbox) DeleteFailed(context.Context, time.Duration, int) (int, error) {
return 0, nil
}
func TestOutboxDispatcherUsesNoopAsDelivered(t *testing.T) {
outbox := &captureDispatchOutbox{items: []store.DispatchOutboxItem{{
ID: 56,
TargetUserID: 1000000002,
Pts: 8,
EventType: domain.UpdateEventNoop,
}}}
events := &captureUpdateEventStore{events: []domain.UpdateEvent{{
UserID: 1000000002,
Type: domain.UpdateEventNoop,
Pts: 8,
Date: 1700000301,
}}}
metrics := &captureOutboxMetrics{}
dispatcher := NewOutboxDispatcher(events, outbox, &captureSessions{}, zaptest.NewLogger(t), WithOutboxMetrics(metrics))
dispatcher.DispatchOnce(context.Background())
if !outbox.delivered || outbox.failed {
t.Fatalf("noop delivered=%v failed=%v, want delivered without push", outbox.delivered, outbox.failed)
}
if metrics.delivered != 1 {
t.Fatalf("noop delivered metrics = %d, want 1", metrics.delivered)
}
}
type captureOutboxMetrics struct {
claimed int
delivered int
failed int
}
func (m *captureOutboxMetrics) MessageSend(time.Duration, bool, error) {}
func (m *captureOutboxMetrics) MessageRateLimited(int) {}
func (m *captureOutboxMetrics) OutboxClaimed(count int) {
m.claimed += count
}
func (m *captureOutboxMetrics) OutboxDelivered(time.Duration) {
m.delivered++
}
func (m *captureOutboxMetrics) OutboxFailed(error) {
m.failed++
}
// queueFullBestEffortSessions 模拟出站队列拥塞:best-effort 推送总是失败(入队超时 / 队列满)。
type queueFullBestEffortSessions struct {
*captureSessions
attempts int
}
func (s *queueFullBestEffortSessions) PushToUserExceptSessionBestEffort(_ context.Context, _ int64, _ int64, _ proto.MessageType, _ bin.Encoder, _ time.Duration) (int, error) {
s.attempts++
return 0, errors.New("mtproto outbound queue full")
}
// TestOutboxDispatcherDefersOnPushQueueFull 验证 best-effort 推送因出站队列拥塞失败时,dispatcher
// 既不标记 delivered(任务保留,靠 dispatching 租约过期重投,满足至少一次投递语义),也不标记
// failed(拥塞不计入 attempts 升级,避免正常满 fan-out 负载把可靠 update 误打成 failed)。
func TestOutboxDispatcherDefersOnPushQueueFull(t *testing.T) {
msg := domain.Message{
ID: 10,
OwnerUserID: 1000000002,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001},
From: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001},
Date: 1700000300,
Body: "hello",
Pts: 7,
}
events := &captureUpdateEventStore{events: []domain.UpdateEvent{{
UserID: msg.OwnerUserID,
Type: domain.UpdateEventNewMessage,
Pts: msg.Pts,
PtsCount: 1,
Date: msg.Date,
Message: msg,
Users: []domain.User{{ID: msg.From.ID, FirstName: "Sender"}},
}}}
outbox := &captureDispatchOutbox{items: []store.DispatchOutboxItem{{
ID: 55,
TargetUserID: msg.OwnerUserID,
Pts: msg.Pts,
EventType: domain.UpdateEventNewMessage,
ExcludeSessionID: 99,
}}}
sessions := &queueFullBestEffortSessions{captureSessions: &captureSessions{}}
metrics := &captureOutboxMetrics{}
dispatcher := NewOutboxDispatcher(events, outbox, sessions, zaptest.NewLogger(t), WithOutboxPushTimeout(50*time.Millisecond), WithOutboxMetrics(metrics))
dispatcher.DispatchOnce(context.Background())
if sessions.attempts != 1 {
t.Fatalf("best-effort push attempts = %d, want 1(应走 best-effort 推送路径)", sessions.attempts)
}
if outbox.delivered {
t.Fatalf("outbox delivered=true, want 未投递(拥塞应保留 dispatching 行靠租约重投)")
}
if outbox.failed {
t.Fatalf("outbox failed=true, want 未失败(拥塞不计入 attempts 升级)")
}
if metrics.failed != 0 {
t.Fatalf("metrics.failed=%d, want 0(拥塞不算投递失败)", metrics.failed)
}
}