merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
262
internal/rpc/outbox_projection_strict_test.go
Normal file
262
internal/rpc/outbox_projection_strict_test.go
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
type capacitySplittingMainOutboxUsers struct {
|
||||
*countingOutboxUsersService
|
||||
maxEdges int
|
||||
capacityErr error
|
||||
sparseRequests []map[int64][]int64
|
||||
}
|
||||
|
||||
func (s *capacitySplittingMainOutboxUsers) ByIDsForViewerUserIDs(ctx context.Context, requested map[int64][]int64) (map[int64][]domain.User, error) {
|
||||
s.sparseRequests = append(s.sparseRequests, cloneOutboxSparseRequest(requested))
|
||||
if s.maxEdges > 0 && sparseOutboxRequestedEdgeCount(requested, s.maxEdges+1) > s.maxEdges {
|
||||
err := s.capacityErr
|
||||
if err == nil {
|
||||
err = store.ErrActiveChannelMemberPairsLimit
|
||||
}
|
||||
return nil, fmt.Errorf("%w: test capacity", err)
|
||||
}
|
||||
return s.countingOutboxUsersService.ByIDsForViewerUserIDs(ctx, requested)
|
||||
}
|
||||
|
||||
type failingSparseMainOutboxResolver struct {
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *failingSparseMainOutboxResolver) ByIDsForViewerUserIDs(context.Context, map[int64][]int64) (map[int64][]domain.User, error) {
|
||||
s.calls++
|
||||
return nil, s.err
|
||||
}
|
||||
|
||||
func TestResolveSparseOutboxUsersSplitsEveryProjectionCapacityError(t *testing.T) {
|
||||
capacityErrors := map[string]error{
|
||||
"privacy_memberships": store.ErrActiveChannelMemberPairsLimit,
|
||||
"owner_union": appusers.ErrBatchUsersLimit,
|
||||
"sparse_cells": appusers.ErrBatchViewerCells,
|
||||
}
|
||||
for name, capacityErr := range capacityErrors {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
base := &countingOutboxUsersService{users: map[int64]domain.User{
|
||||
2001: {ID: 2001, FirstName: "one"},
|
||||
2002: {ID: 2002, FirstName: "two"},
|
||||
}}
|
||||
resolver := &capacitySplittingMainOutboxUsers{
|
||||
countingOutboxUsersService: base,
|
||||
maxEdges: 1,
|
||||
capacityErr: capacityErr,
|
||||
}
|
||||
projected, err := resolveSparseOutboxUsers(context.Background(), resolver, map[int64][]int64{
|
||||
1001: {2001},
|
||||
1002: {2002},
|
||||
})
|
||||
if err != nil || len(projected[1001]) != 1 || len(projected[1002]) != 1 {
|
||||
t.Fatalf("projected=%+v err=%v, want both split results", projected, err)
|
||||
}
|
||||
if len(resolver.sparseRequests) != 3 || len(base.calls) != 0 {
|
||||
t.Fatalf("sparse calls=%d scalar=%+v, want one failed batch plus two sparse halves", len(resolver.sparseRequests), base.calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSparseOutboxUsersDoesNotSplitOtherErrors(t *testing.T) {
|
||||
boom := errors.New("projection unavailable")
|
||||
resolver := &failingSparseMainOutboxResolver{err: boom}
|
||||
projected, err := resolveSparseOutboxUsers(context.Background(), resolver, map[int64][]int64{
|
||||
1001: {2001},
|
||||
1002: {2002},
|
||||
})
|
||||
if !errors.Is(err, boom) || projected != nil {
|
||||
t.Fatalf("resolveSparseOutboxUsers = %+v, %v; want nil, boom", projected, err)
|
||||
}
|
||||
if resolver.calls != 1 {
|
||||
t.Fatalf("resolver calls = %d, want no split for non-capacity error", resolver.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterBuildOutboxUpdatesFailsClosedAtSparseRecoveryCallBudget(t *testing.T) {
|
||||
const viewerID = int64(1000000050)
|
||||
base := &countingOutboxUsersService{users: make(map[int64]domain.User)}
|
||||
peers := make([]domain.Peer, 100)
|
||||
for i := range peers {
|
||||
id := int64(1000001000 + i)
|
||||
peers[i] = domain.Peer{Type: domain.PeerTypeUser, ID: id}
|
||||
base.users[id] = domain.User{ID: id, FirstName: "projected"}
|
||||
}
|
||||
users := &capacitySplittingMainOutboxUsers{
|
||||
countingOutboxUsersService: base,
|
||||
maxEdges: 1,
|
||||
capacityErr: store.ErrActiveChannelMemberPairsLimit,
|
||||
}
|
||||
router := New(Config{}, Deps{Users: users}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
updates, err := router.BuildOutboxUpdates(context.Background(), []OutboxUpdateRequest{{
|
||||
TargetUserID: viewerID,
|
||||
Event: domain.UpdateEvent{
|
||||
UserID: viewerID,
|
||||
Type: domain.UpdateEventPinnedDialogs,
|
||||
Peers: peers,
|
||||
},
|
||||
}})
|
||||
if updates != nil || !errors.Is(err, ErrUserProjectionCapacityRecoveryLimit) {
|
||||
t.Fatalf("updates=%+v err=%v, want nil recovery-limit error", updates, err)
|
||||
}
|
||||
if errors.Is(err, store.ErrActiveChannelMemberPairsLimit) {
|
||||
t.Fatalf("recovery-limit error must not retain capacity identity: %v", err)
|
||||
}
|
||||
if len(users.sparseRequests) != maxSparseOutboxRecoveryCalls {
|
||||
t.Fatalf("sparse resolver calls=%d, want hard limit %d", len(users.sparseRequests), maxSparseOutboxRecoveryCalls)
|
||||
}
|
||||
if len(base.calls) != 0 {
|
||||
t.Fatalf("scalar ByIDs calls=%+v, want zero fallback", base.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSparseOutboxUsersRejectsAttemptedEdgeOverflowBeforeResolver(t *testing.T) {
|
||||
makeIDs := func(count int) []int64 {
|
||||
ids := make([]int64, count)
|
||||
for i := range ids {
|
||||
ids[i] = int64(i + 1)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
t.Run("initial request", func(t *testing.T) {
|
||||
resolver := &failingSparseMainOutboxResolver{err: errors.New("must not be called")}
|
||||
projected, err := resolveSparseOutboxUsers(context.Background(), resolver, map[int64][]int64{
|
||||
1001: makeIDs(maxSparseOutboxAttemptedUserEdges + 1),
|
||||
})
|
||||
if projected != nil || !errors.Is(err, ErrUserProjectionCapacityRecoveryLimit) {
|
||||
t.Fatalf("projected=%+v err=%v, want nil recovery-limit error", projected, err)
|
||||
}
|
||||
if resolver.calls != 0 {
|
||||
t.Fatalf("resolver calls=%d, want rejection before first call", resolver.calls)
|
||||
}
|
||||
if isSparseProjectionCapacityError(err) {
|
||||
t.Fatalf("recovery-limit error must be terminal, got capacity identity: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cumulative recursive edges", func(t *testing.T) {
|
||||
resolver := &failingSparseMainOutboxResolver{err: store.ErrActiveChannelMemberPairsLimit}
|
||||
projected, err := resolveSparseOutboxUsers(context.Background(), resolver, map[int64][]int64{
|
||||
1001: makeIDs(300000),
|
||||
})
|
||||
if projected != nil || !errors.Is(err, ErrUserProjectionCapacityRecoveryLimit) {
|
||||
t.Fatalf("projected=%+v err=%v, want nil recovery-limit error", projected, err)
|
||||
}
|
||||
if resolver.calls != 2 {
|
||||
t.Fatalf("resolver calls=%d, want root and first half before shared edge limit", resolver.calls)
|
||||
}
|
||||
if errors.Is(err, store.ErrActiveChannelMemberPairsLimit) {
|
||||
t.Fatalf("recovery-limit error must not retain capacity identity: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouterBuildOutboxUpdatesRejectsIncompleteSparseProjection(t *testing.T) {
|
||||
const (
|
||||
viewerUserID = int64(1000000010)
|
||||
missingUserID = int64(1000000011)
|
||||
)
|
||||
users := &countingOutboxUsersService{users: map[int64]domain.User{}}
|
||||
router := New(Config{}, Deps{Users: users}, zaptest.NewLogger(t), clock.System)
|
||||
updates, err := router.BuildOutboxUpdates(context.Background(), []OutboxUpdateRequest{{
|
||||
TargetUserID: viewerUserID,
|
||||
Event: domain.UpdateEvent{UserID: viewerUserID, Type: domain.UpdateEventNewMessage, Pts: 3, PtsCount: 1,
|
||||
Message: domain.Message{ID: 3, OwnerUserID: viewerUserID,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: missingUserID},
|
||||
From: domain.Peer{Type: domain.PeerTypeUser, ID: missingUserID}}},
|
||||
}})
|
||||
if !errors.Is(err, ErrSparseOutboxUserProjectionIncomplete) || updates != nil {
|
||||
t.Fatalf("BuildOutboxUpdates=%+v err=%v, want incomplete fail-closed", updates, err)
|
||||
}
|
||||
if users.sparseCalls != 1 || len(users.calls) != 0 {
|
||||
t.Fatalf("projection calls = sparse %d scalar %+v, want one sparse call and no fallback", users.sparseCalls, users.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterBuildOutboxUpdatesAllowsOnlyExplicitNoopToStayEmpty(t *testing.T) {
|
||||
router := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
|
||||
updates, err := router.BuildOutboxUpdates(context.Background(), []OutboxUpdateRequest{{
|
||||
TargetUserID: 1000000030,
|
||||
Event: domain.UpdateEvent{UserID: 1000000030, Type: domain.UpdateEventNoop, Pts: 9, PtsCount: 1},
|
||||
}})
|
||||
if err != nil || len(updates) != 1 || updates[0] != nil {
|
||||
t.Fatalf("explicit noop updates=%+v err=%v, want one nil entry without error", updates, err)
|
||||
}
|
||||
|
||||
updates, err = router.BuildOutboxUpdates(context.Background(), []OutboxUpdateRequest{{
|
||||
TargetUserID: 1000000030,
|
||||
Event: domain.UpdateEvent{UserID: 1000000030, Type: domain.UpdateEventReadHistoryInbox, Pts: 10, PtsCount: 1},
|
||||
}})
|
||||
if !errors.Is(err, ErrOutboxUpdateProjectionEmpty) || updates != nil {
|
||||
t.Fatalf("invalid non-noop updates=%+v err=%v, want ErrOutboxUpdateProjectionEmpty", updates, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboxDispatcherFailsClosedOnBuilderErrorAndNilNonNoop(t *testing.T) {
|
||||
const userID = int64(1000000040)
|
||||
baseEvent := domain.UpdateEvent{
|
||||
UserID: userID, Type: domain.UpdateEventReadHistoryInbox, Pts: 10, PtsCount: 1,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000041}, MaxID: 9,
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
builder OutboxUpdateBuilder
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "builder error",
|
||||
builder: func(context.Context, []OutboxUpdateRequest) ([]*tg.Updates, error) {
|
||||
return nil, errors.New("projection unavailable")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil non-noop",
|
||||
builder: func(_ context.Context, requests []OutboxUpdateRequest) ([]*tg.Updates, error) {
|
||||
return make([]*tg.Updates, len(requests)), nil
|
||||
},
|
||||
wantErr: errOutboxUpdateBuilderEmpty,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
outbox := &captureDispatchOutbox{items: []store.DispatchOutboxItem{{
|
||||
ID: 71, TargetUserID: userID, Pts: baseEvent.Pts, EventType: baseEvent.Type,
|
||||
}}}
|
||||
sessions := &captureSessions{}
|
||||
dispatcher := newTestOutboxDispatcher(
|
||||
&captureUpdateEventStore{events: []domain.UpdateEvent{baseEvent}},
|
||||
outbox,
|
||||
sessions,
|
||||
zaptest.NewLogger(t),
|
||||
WithOutboxUpdateBuilder(tt.builder),
|
||||
)
|
||||
dispatcher.DispatchOnce(context.Background())
|
||||
if !outbox.failed || outbox.delivered || sessions.message != nil {
|
||||
t.Fatalf("failed=%v delivered=%v pushed=%T, want terminal failure without delivery", outbox.failed, outbox.delivered, sessions.message)
|
||||
}
|
||||
if tt.wantErr != nil && !strings.Contains(outbox.failedError, tt.wantErr.Error()) {
|
||||
t.Fatalf("failed error=%q, want %v", outbox.failedError, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue