merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
|
|
@ -2,9 +2,12 @@ package userprojection
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
|
|
@ -19,6 +22,7 @@ type blockingFirstListContactStore struct {
|
|||
|
||||
mu sync.Mutex
|
||||
firstUsed bool
|
||||
listCalls int
|
||||
}
|
||||
|
||||
type blockingFirstPersonalPhotoStore struct {
|
||||
|
|
@ -31,8 +35,56 @@ type blockingFirstPersonalPhotoStore struct {
|
|||
firstUsed bool
|
||||
}
|
||||
|
||||
type stalePersonalPhotoWritebackContextKey struct{}
|
||||
|
||||
// stalePersonalPhotoWritebackStore deterministically models an older mutation
|
||||
// that commits first but returns to the cache wrapper after a newer mutation.
|
||||
// The old implementation performed a post-commit PersonalPhotos read and could
|
||||
// publish this captured old value after the newer mutation had completed.
|
||||
type stalePersonalPhotoWritebackStore struct {
|
||||
store.ContactStore
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
|
||||
mu sync.Mutex
|
||||
staleReadCalls int
|
||||
}
|
||||
|
||||
func (s *stalePersonalPhotoWritebackStore) SetPersonalPhoto(ctx context.Context, userID, contactUserID int64, photoID int64, date int) (domain.Contact, bool, error) {
|
||||
contact, found, err := s.ContactStore.SetPersonalPhoto(ctx, userID, contactUserID, photoID, date)
|
||||
if err != nil || !found || ctx.Value(stalePersonalPhotoWritebackContextKey{}) != true {
|
||||
return contact, found, err
|
||||
}
|
||||
close(s.started)
|
||||
select {
|
||||
case <-s.release:
|
||||
case <-ctx.Done():
|
||||
return domain.Contact{}, false, ctx.Err()
|
||||
}
|
||||
return contact, found, nil
|
||||
}
|
||||
|
||||
func (s *stalePersonalPhotoWritebackStore) PersonalPhotos(ctx context.Context, userID int64, contactUserIDs []int64) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
if len(contactUserIDs) > 0 && ctx.Value(stalePersonalPhotoWritebackContextKey{}) == true {
|
||||
s.mu.Lock()
|
||||
s.staleReadCalls++
|
||||
s.mu.Unlock()
|
||||
return map[int64]domain.ProfilePhotoRef{
|
||||
contactUserIDs[0]: {PhotoID: 9001, Personal: true},
|
||||
}, nil
|
||||
}
|
||||
return s.ContactStore.PersonalPhotos(ctx, userID, contactUserIDs)
|
||||
}
|
||||
|
||||
func (s *stalePersonalPhotoWritebackStore) staleReads() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.staleReadCalls
|
||||
}
|
||||
|
||||
func (s *blockingFirstListContactStore) ListByUser(ctx context.Context, userID int64) (domain.ContactList, error) {
|
||||
s.mu.Lock()
|
||||
s.listCalls++
|
||||
if !s.firstUsed {
|
||||
s.firstUsed = true
|
||||
s.mu.Unlock()
|
||||
|
|
@ -48,6 +100,12 @@ func (s *blockingFirstListContactStore) ListByUser(ctx context.Context, userID i
|
|||
return s.ContactStore.ListByUser(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *blockingFirstListContactStore) callCount() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.listCalls
|
||||
}
|
||||
|
||||
func (s *blockingFirstPersonalPhotoStore) PersonalPhotos(ctx context.Context, userID int64, contactUserIDs []int64) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
s.mu.Lock()
|
||||
if !s.firstUsed {
|
||||
|
|
@ -84,6 +142,7 @@ type countingContactStore struct {
|
|||
listCalls int
|
||||
getManyCalls int
|
||||
reverseCalls int
|
||||
projectionCalls int
|
||||
personalPhotoCalls int
|
||||
setPersonalPhotoHit int
|
||||
}
|
||||
|
|
@ -103,6 +162,11 @@ func (s *countingContactStore) GetReverseContacts(ctx context.Context, userID in
|
|||
return s.ContactStore.GetReverseContacts(ctx, userID, ownerUserIDs)
|
||||
}
|
||||
|
||||
func (s *countingContactStore) ContactProjectionForViewers(ctx context.Context, viewerUserIDs, contactUserIDs []int64) (domain.ContactProjectionBatch, error) {
|
||||
s.projectionCalls++
|
||||
return s.ContactStore.ContactProjectionForViewers(ctx, viewerUserIDs, contactUserIDs)
|
||||
}
|
||||
|
||||
func (s *countingContactStore) PersonalPhotos(ctx context.Context, userID int64, contactUserIDs []int64) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
s.personalPhotoCalls++
|
||||
return s.ContactStore.PersonalPhotos(ctx, userID, contactUserIDs)
|
||||
|
|
@ -162,6 +226,413 @@ func TestCachedContactStoreCachesProjectionReads(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreContactSnapshotLRUEvictsOnlyOldestViewer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := memory.NewContactStore()
|
||||
for viewerID := int64(1); viewerID <= 3; viewerID++ {
|
||||
if _, err := base.Upsert(ctx, viewerID, domain.ContactInput{
|
||||
ContactUserID: 100 + viewerID,
|
||||
FirstName: fmt.Sprintf("viewer-%d", viewerID),
|
||||
}); err != nil {
|
||||
t.Fatalf("seed viewer %d: %v", viewerID, err)
|
||||
}
|
||||
}
|
||||
counting := &countingContactStore{ContactStore: base}
|
||||
cached := NewCachedContactStoreWithMaxViewers(counting, time.Hour, 2)
|
||||
|
||||
for _, viewerID := range []int64{1, 2, 1, 3} {
|
||||
if _, err := cached.ListByUser(ctx, viewerID); err != nil {
|
||||
t.Fatalf("list viewer %d: %v", viewerID, err)
|
||||
}
|
||||
}
|
||||
if counting.listCalls != 3 {
|
||||
t.Fatalf("ListByUser calls = %d, want 3 before evicted viewer is read", counting.listCalls)
|
||||
}
|
||||
cached.mu.RLock()
|
||||
_, hasOne := cached.contacts[1]
|
||||
_, hasTwo := cached.contacts[2]
|
||||
_, hasThree := cached.contacts[3]
|
||||
contactEntries := cached.contactLRU.Len()
|
||||
cached.mu.RUnlock()
|
||||
if !hasOne || hasTwo || !hasThree || contactEntries != 2 {
|
||||
t.Fatalf("contact LRU state = one:%v two:%v three:%v len:%d, want one+three only", hasOne, hasTwo, hasThree, contactEntries)
|
||||
}
|
||||
|
||||
if _, err := cached.ListByUser(ctx, 1); err != nil {
|
||||
t.Fatalf("list retained viewer 1: %v", err)
|
||||
}
|
||||
if counting.listCalls != 3 {
|
||||
t.Fatalf("retained viewer caused cold load: calls=%d, want 3", counting.listCalls)
|
||||
}
|
||||
if _, err := cached.ListByUser(ctx, 2); err != nil {
|
||||
t.Fatalf("list evicted viewer 2: %v", err)
|
||||
}
|
||||
if counting.listCalls != 4 {
|
||||
t.Fatalf("evicted viewer did not cold load exactly once: calls=%d, want 4", counting.listCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreContactAndPersonalPhotoLRUsAreIndependent(t *testing.T) {
|
||||
cached := NewCachedContactStoreWithMaxViewers(memory.NewContactStore(), time.Hour, 2)
|
||||
expireAt := time.Now().Add(time.Hour)
|
||||
contactSnap := func(userID int64) contactAccountSnapshot {
|
||||
return buildContactAccountSnapshot(domain.ContactList{Contacts: []domain.Contact{{
|
||||
User: domain.User{ID: 100 + userID},
|
||||
}}}, expireAt)
|
||||
}
|
||||
photoSnap := func(userID int64) personalPhotoSnapshot {
|
||||
return personalPhotoSnapshot{
|
||||
refs: map[int64]domain.ProfilePhotoRef{100 + userID: {PhotoID: 9000 + userID}},
|
||||
expireAt: expireAt,
|
||||
}
|
||||
}
|
||||
|
||||
cached.mu.Lock()
|
||||
cached.storeContactSnapshotLocked(1, contactSnap(1))
|
||||
cached.storeContactSnapshotLocked(2, contactSnap(2))
|
||||
cached.storePersonalPhotoSnapshotLocked(1, photoSnap(1))
|
||||
cached.storePersonalPhotoSnapshotLocked(2, photoSnap(2))
|
||||
cached.mu.Unlock()
|
||||
if _, ok := cached.lookupContactSnapshot(1, time.Now()); !ok {
|
||||
t.Fatal("contact viewer 1 missing before LRU touch")
|
||||
}
|
||||
cached.mu.Lock()
|
||||
cached.storeContactSnapshotLocked(3, contactSnap(3))
|
||||
cached.mu.Unlock()
|
||||
|
||||
cached.mu.RLock()
|
||||
_, contactOne := cached.contacts[1]
|
||||
_, contactTwo := cached.contacts[2]
|
||||
_, contactThree := cached.contacts[3]
|
||||
_, photoOne := cached.personalPhotos[1]
|
||||
_, photoTwo := cached.personalPhotos[2]
|
||||
cached.mu.RUnlock()
|
||||
if !contactOne || contactTwo || !contactThree {
|
||||
t.Fatalf("contact LRU = one:%v two:%v three:%v, want one+three", contactOne, contactTwo, contactThree)
|
||||
}
|
||||
if !photoOne || !photoTwo {
|
||||
t.Fatalf("contact eviction crossed into personal-photo LRU: one:%v two:%v", photoOne, photoTwo)
|
||||
}
|
||||
|
||||
if _, ok := cached.lookupPersonalPhotoSnapshot(2, time.Now()); !ok {
|
||||
t.Fatal("personal-photo viewer 2 missing before LRU touch")
|
||||
}
|
||||
cached.mu.Lock()
|
||||
cached.storePersonalPhotoSnapshotLocked(3, photoSnap(3))
|
||||
cached.mu.Unlock()
|
||||
cached.mu.RLock()
|
||||
_, photoOne = cached.personalPhotos[1]
|
||||
_, photoTwo = cached.personalPhotos[2]
|
||||
_, photoThree := cached.personalPhotos[3]
|
||||
_, contactOne = cached.contacts[1]
|
||||
_, contactThree = cached.contacts[3]
|
||||
cached.mu.RUnlock()
|
||||
if photoOne || !photoTwo || !photoThree {
|
||||
t.Fatalf("personal-photo LRU = one:%v two:%v three:%v, want two+three", photoOne, photoTwo, photoThree)
|
||||
}
|
||||
if !contactOne || !contactThree {
|
||||
t.Fatalf("personal-photo eviction crossed into contact LRU: one:%v three:%v", contactOne, contactThree)
|
||||
}
|
||||
|
||||
cached.InvalidateViewers(3)
|
||||
cached.mu.RLock()
|
||||
_, contactThree = cached.contacts[3]
|
||||
_, photoThree = cached.personalPhotos[3]
|
||||
_, contactElement := cached.contactElements[3]
|
||||
_, photoElement := cached.personalElements[3]
|
||||
cached.mu.RUnlock()
|
||||
if contactThree || photoThree || contactElement || photoElement {
|
||||
t.Fatalf("viewer invalidation left LRU state: contact=%v photo=%v contactElement=%v photoElement=%v",
|
||||
contactThree, photoThree, contactElement, photoElement)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreUnrelatedViewerInvalidationDoesNotRejectRefill(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := memory.NewContactStore()
|
||||
if _, err := base.Upsert(ctx, 2, domain.ContactInput{ContactUserID: 20, FirstName: "current"}); err != nil {
|
||||
t.Fatalf("seed current contact: %v", err)
|
||||
}
|
||||
blocking := &blockingFirstListContactStore{
|
||||
ContactStore: base,
|
||||
started: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
first: domain.ContactList{Contacts: []domain.Contact{{
|
||||
User: domain.User{ID: 20},
|
||||
FirstName: "captured",
|
||||
}}},
|
||||
}
|
||||
cached := NewCachedContactStore(blocking, time.Hour)
|
||||
|
||||
type readResult struct {
|
||||
contacts map[int64]domain.Contact
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan readResult, 1)
|
||||
go func() {
|
||||
contacts, err := cached.GetMany(ctx, 2, []int64{20})
|
||||
resultCh <- readResult{contacts: contacts, err: err}
|
||||
}()
|
||||
waitForCacheTestSignal(t, blocking.started)
|
||||
cached.InvalidateViewers(1)
|
||||
close(blocking.release)
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
if result.err != nil {
|
||||
t.Fatalf("contact read: %v", result.err)
|
||||
}
|
||||
if got := result.contacts[20].FirstName; got != "captured" {
|
||||
t.Fatalf("unrelated invalidation rejected captured refill: got %q", got)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for contact read")
|
||||
}
|
||||
if calls := blocking.callCount(); calls != 1 {
|
||||
t.Fatalf("ListByUser calls = %d, want 1 after unrelated invalidation", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreContactProjectionForViewersUsesViewerOwnedPairCache(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := memory.NewContactStore()
|
||||
if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Alice"}); err != nil {
|
||||
t.Fatalf("seed viewer 1 contact: %v", err)
|
||||
}
|
||||
if _, _, err := base.SetPersonalPhoto(ctx, 1, 2, 9101, 100); err != nil {
|
||||
t.Fatalf("seed viewer 1 personal photo: %v", err)
|
||||
}
|
||||
if _, err := base.Upsert(ctx, 3, domain.ContactInput{ContactUserID: 2, FirstName: "Bob"}); err != nil {
|
||||
t.Fatalf("seed viewer 3 contact: %v", err)
|
||||
}
|
||||
if _, _, err := base.SetPersonalPhoto(ctx, 3, 2, 9103, 100); err != nil {
|
||||
t.Fatalf("seed viewer 3 personal photo: %v", err)
|
||||
}
|
||||
counting := &countingContactStore{ContactStore: base}
|
||||
cached := NewCachedContactStore(counting, 0)
|
||||
|
||||
if _, err := cached.GetMany(ctx, 1, []int64{2}); err != nil {
|
||||
t.Fatalf("prime viewer 1 contacts: %v", err)
|
||||
}
|
||||
if _, err := cached.PersonalPhotos(ctx, 1, []int64{2}); err != nil {
|
||||
t.Fatalf("prime viewer 1 photos: %v", err)
|
||||
}
|
||||
|
||||
first, err := cached.ContactProjectionForViewers(ctx, []int64{1, 3}, []int64{2})
|
||||
if err != nil {
|
||||
t.Fatalf("first projection: %v", err)
|
||||
}
|
||||
if first.Contacts[1][2].FirstName != "Alice" || first.PersonalPhotos[1][2].PhotoID != 9101 {
|
||||
t.Fatalf("viewer 1 projection = %+v %+v, want warm Alice/9101", first.Contacts[1][2], first.PersonalPhotos[1][2])
|
||||
}
|
||||
if first.Contacts[3][2].FirstName != "Bob" || first.PersonalPhotos[3][2].PhotoID != 9103 {
|
||||
t.Fatalf("viewer 3 projection = %+v %+v, want cold Bob/9103", first.Contacts[3][2], first.PersonalPhotos[3][2])
|
||||
}
|
||||
if counting.projectionCalls != 1 {
|
||||
t.Fatalf("projection calls after first = %d, want 1", counting.projectionCalls)
|
||||
}
|
||||
|
||||
second, err := cached.ContactProjectionForViewers(ctx, []int64{3}, []int64{2})
|
||||
if err != nil {
|
||||
t.Fatalf("second projection: %v", err)
|
||||
}
|
||||
if second.Contacts[3][2].FirstName != "Bob" || second.PersonalPhotos[3][2].PhotoID != 9103 {
|
||||
t.Fatalf("cached viewer 3 projection = %+v %+v, want Bob/9103", second.Contacts[3][2], second.PersonalPhotos[3][2])
|
||||
}
|
||||
if counting.projectionCalls != 1 {
|
||||
t.Fatalf("projection calls after cached read = %d, want 1", counting.projectionCalls)
|
||||
}
|
||||
|
||||
cached.InvalidateViewers(3)
|
||||
if _, err := cached.ContactProjectionForViewers(ctx, []int64{3}, []int64{2}); err != nil {
|
||||
t.Fatalf("projection after invalidation: %v", err)
|
||||
}
|
||||
if counting.projectionCalls != 2 {
|
||||
t.Fatalf("projection calls after invalidation = %d, want 2", counting.projectionCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStorePairSnapshotsAreCompact(t *testing.T) {
|
||||
pointerSize := unsafe.Sizeof(uintptr(0))
|
||||
timeSize := unsafe.Sizeof(time.Time{})
|
||||
if got, max := unsafe.Sizeof(reverseContactSnapshot{}), timeSize+2*pointerSize; got > max {
|
||||
t.Fatalf("reverseContactSnapshot size = %d, want <= %d (one value pointer plus expiry)", got, max)
|
||||
}
|
||||
if got, max := unsafe.Sizeof(contactProjectionSnapshot{}), timeSize+3*pointerSize; got > max {
|
||||
t.Fatalf("contactProjectionSnapshot size = %d, want <= %d (two value pointers plus expiry)", got, max)
|
||||
}
|
||||
if got, large := unsafe.Sizeof(reverseContactSnapshot{}), unsafe.Sizeof(domain.Contact{}); got >= large {
|
||||
t.Fatalf("reverseContactSnapshot size = %d, must not embed %d-byte domain.Contact", got, large)
|
||||
}
|
||||
if got, large := unsafe.Sizeof(contactProjectionSnapshot{}), unsafe.Sizeof(domain.Contact{}); got >= large {
|
||||
t.Fatalf("contactProjectionSnapshot size = %d, must not embed %d-byte domain.Contact", got, large)
|
||||
}
|
||||
if got, max := unsafe.Sizeof(cachedContactProjectionOverlay{}), uintptr(128); got > max {
|
||||
t.Fatalf("cachedContactProjectionOverlay size = %d, want <= %d bytes", got, max)
|
||||
}
|
||||
if got, large := unsafe.Sizeof(cachedContactProjectionOverlay{}), unsafe.Sizeof(domain.Contact{}); got >= large {
|
||||
t.Fatalf("cachedContactProjectionOverlay size = %d, must be smaller than %d-byte domain.Contact", got, large)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStorePairSnapshotsUseNilForNegativeAndClonePositiveValues(t *testing.T) {
|
||||
cached := NewCachedContactStore(memory.NewContactStore(), time.Hour)
|
||||
now := time.Unix(1000, 0)
|
||||
expireAt := now.Add(time.Hour)
|
||||
contact := domain.Contact{
|
||||
User: domain.User{
|
||||
ID: 2, AccessHash: 2002, Phone: "global-phone", FirstName: "Global", LastName: "User",
|
||||
Username: "global_user", Mutual: true, PhotoStripped: []byte{1, 2, 3},
|
||||
},
|
||||
FirstName: "Local",
|
||||
LastName: "Name",
|
||||
Phone: "known-phone",
|
||||
Note: "private note",
|
||||
NoteEntities: []domain.MessageEntity{{
|
||||
Type: domain.MessageEntityBold, Offset: 0, Length: 3,
|
||||
}},
|
||||
CloseFriend: true,
|
||||
}
|
||||
photo := domain.ProfilePhotoRef{PhotoID: 9001, Stripped: []byte{4, 5, 6}, Personal: true}
|
||||
positiveReverseKey := reverseContactKey{ownerUserID: 1, contactUserID: 2}
|
||||
negativeReverseKey := reverseContactKey{ownerUserID: 3, contactUserID: 2}
|
||||
positiveProjectionKey := contactProjectionKey{viewerUserID: 1, contactUserID: 2}
|
||||
negativeProjectionKey := contactProjectionKey{viewerUserID: 1, contactUserID: 99}
|
||||
|
||||
cached.mu.Lock()
|
||||
cached.storeReverseContactLocked(positiveReverseKey, contact, true, expireAt)
|
||||
cached.storeReverseContactLocked(negativeReverseKey, contact, false, expireAt)
|
||||
cached.storeContactProjectionPairLocked(positiveProjectionKey, contact, true, photo, true, expireAt)
|
||||
cached.storeContactProjectionPairLocked(negativeProjectionKey, contact, false, photo, false, expireAt)
|
||||
positiveReverse := cached.reverse[positiveReverseKey].Value.(*reverseContactEntry).snapshot
|
||||
negativeReverse := cached.reverse[negativeReverseKey].Value.(*reverseContactEntry).snapshot
|
||||
positiveProjection := cached.projection[positiveProjectionKey].Value.(*contactProjectionEntry).snapshot
|
||||
negativeProjection := cached.projection[negativeProjectionKey].Value.(*contactProjectionEntry).snapshot
|
||||
cached.mu.Unlock()
|
||||
|
||||
if positiveReverse.contact == nil || positiveProjection.contact == nil || positiveProjection.personalPhoto == nil {
|
||||
t.Fatalf("positive snapshots lost values: reverse=%+v projection=%+v", positiveReverse, positiveProjection)
|
||||
}
|
||||
if negativeReverse.contact != nil || negativeProjection.contact != nil || negativeProjection.personalPhoto != nil {
|
||||
t.Fatalf("negative snapshots retained value allocations: reverse=%+v projection=%+v", negativeReverse, negativeProjection)
|
||||
}
|
||||
|
||||
// Publication clones inputs; subsequent caller mutation cannot alter cache.
|
||||
contact.User.PhotoStripped[0] = 10
|
||||
contact.NoteEntities[0].Length = 10
|
||||
photo.Stripped[0] = 10
|
||||
|
||||
reverse, found, hit := cached.lookupReverseContact(1, 2, now)
|
||||
if !hit || !found || reverse.User.PhotoStripped[0] != 1 || reverse.NoteEntities[0].Length != 3 {
|
||||
t.Fatalf("positive reverse lookup = %+v found=%v hit=%v", reverse, found, hit)
|
||||
}
|
||||
reverse.User.PhotoStripped[0] = 11
|
||||
reverse.NoteEntities[0].Length = 11
|
||||
reverseAgain, found, hit := cached.lookupReverseContact(1, 2, now)
|
||||
if !hit || !found || reverseAgain.User.PhotoStripped[0] != 1 || reverseAgain.NoteEntities[0].Length != 3 {
|
||||
t.Fatalf("reverse lookup shared mutable slices: %+v found=%v hit=%v", reverseAgain, found, hit)
|
||||
}
|
||||
if _, found, hit := cached.lookupReverseContact(3, 2, now); !hit || found {
|
||||
t.Fatalf("negative reverse lookup found=%v hit=%v, want false/true", found, hit)
|
||||
}
|
||||
|
||||
pair, hit := cached.lookupContactProjectionPair(1, 2, now)
|
||||
if !hit || !pair.contactFound || !pair.personalPhotoFound || pair.personalPhoto.Stripped[0] != 4 {
|
||||
t.Fatalf("positive projection lookup = %+v hit=%v", pair, hit)
|
||||
}
|
||||
if !reflect.DeepEqual(pair.contact.User, domain.User{ID: 2}) {
|
||||
t.Fatalf("projection pair retained base user data: %+v", pair.contact.User)
|
||||
}
|
||||
if pair.contact.FirstName != "Local" || pair.contact.LastName != "Name" || pair.contact.Phone != "known-phone" ||
|
||||
pair.contact.Note != "private note" || !pair.contact.Mutual || !pair.contact.CloseFriend {
|
||||
t.Fatalf("projection pair lost viewer-owned overlay: %+v", pair.contact)
|
||||
}
|
||||
pair.contact.NoteEntities[0].Length = 12
|
||||
pair.personalPhoto.Stripped[0] = 12
|
||||
pairAgain, hit := cached.lookupContactProjectionPair(1, 2, now)
|
||||
if !hit || !reflect.DeepEqual(pairAgain.contact.User, domain.User{ID: 2}) || pairAgain.contact.NoteEntities[0].Length != 3 || pairAgain.personalPhoto.Stripped[0] != 4 {
|
||||
t.Fatalf("projection lookup shared mutable slices: %+v hit=%v", pairAgain, hit)
|
||||
}
|
||||
negative, hit := cached.lookupContactProjectionPair(1, 99, now)
|
||||
if !hit || negative.contactFound || negative.personalPhotoFound {
|
||||
t.Fatalf("negative projection lookup = %+v hit=%v, want cached miss", negative, hit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreLargeDenseProjectionDoesNotPollutePairCache(t *testing.T) {
|
||||
if !admitDenseContactProjectionPairs(1, contactProjectionDenseAdmissionMaxCells) {
|
||||
t.Fatal("admission rejected the documented cell limit")
|
||||
}
|
||||
if admitDenseContactProjectionPairs(1, contactProjectionDenseAdmissionMaxCells+1) {
|
||||
t.Fatal("admission accepted a batch above the documented cell limit")
|
||||
}
|
||||
|
||||
counting := &countingContactStore{ContactStore: memory.NewContactStore()}
|
||||
cached := NewCachedContactStore(counting, time.Hour)
|
||||
seedKey := contactProjectionKey{viewerUserID: 1, contactUserID: 2}
|
||||
cached.mu.Lock()
|
||||
cached.storeContactProjectionPairLocked(
|
||||
seedKey,
|
||||
domain.Contact{User: domain.User{ID: 2}, FirstName: "seed"}, true,
|
||||
domain.ProfilePhotoRef{}, false,
|
||||
cached.now().Add(time.Hour),
|
||||
)
|
||||
cached.mu.Unlock()
|
||||
|
||||
viewers := []int64{1001, 1002}
|
||||
targets := make([]int64, contactProjectionDenseAdmissionMaxCells/len(viewers)+1)
|
||||
for i := range targets {
|
||||
targets[i] = int64(100000 + i)
|
||||
}
|
||||
for call := 1; call <= 2; call++ {
|
||||
got, err := cached.ContactProjectionForViewers(context.Background(), viewers, targets)
|
||||
if err != nil {
|
||||
t.Fatalf("large dense projection call %d: %v", call, err)
|
||||
}
|
||||
if len(got.Contacts) != 0 || len(got.PersonalPhotos) != 0 {
|
||||
t.Fatalf("large empty projection call %d = %+v", call, got)
|
||||
}
|
||||
cached.mu.Lock()
|
||||
_, seedPresent := cached.projection[seedKey]
|
||||
pairCount := len(cached.projection)
|
||||
lruCount := cached.projectionLRU.Len()
|
||||
cached.mu.Unlock()
|
||||
if !seedPresent || pairCount != 1 || lruCount != 1 {
|
||||
t.Fatalf("large dense load polluted pair cache: seed=%v pairs=%d lru=%d", seedPresent, pairCount, lruCount)
|
||||
}
|
||||
}
|
||||
if counting.projectionCalls != 2 {
|
||||
t.Fatalf("projection calls = %d, want 2 because oversized results are returned but not admitted", counting.projectionCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreContactProjectionSkipsColdReadForKnownNonContact(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := memory.NewContactStore()
|
||||
if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Alice"}); err != nil {
|
||||
t.Fatalf("seed contact: %v", err)
|
||||
}
|
||||
counting := &countingContactStore{ContactStore: base}
|
||||
cached := NewCachedContactStore(counting, 0)
|
||||
|
||||
if _, err := cached.GetMany(ctx, 1, []int64{99}); err != nil {
|
||||
t.Fatalf("prime viewer contact snapshot: %v", err)
|
||||
}
|
||||
got, err := cached.ContactProjectionForViewers(ctx, []int64{1}, []int64{99})
|
||||
if err != nil {
|
||||
t.Fatalf("projection: %v", err)
|
||||
}
|
||||
if len(got.Contacts[1]) != 0 || len(got.PersonalPhotos[1]) != 0 {
|
||||
t.Fatalf("known non-contact projection = %+v", got)
|
||||
}
|
||||
if counting.projectionCalls != 0 {
|
||||
t.Fatalf("projection calls = %d, want 0 for known non-contact", counting.projectionCalls)
|
||||
}
|
||||
if counting.personalPhotoCalls != 0 {
|
||||
t.Fatalf("personal photo calls = %d, want 0 for known non-contact", counting.personalPhotoCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreCachesLargeReverseContactBatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := memory.NewContactStore()
|
||||
|
|
@ -237,7 +708,7 @@ func TestCachedContactStoreReversePairsUsePerEntryLRU(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreInvalidatesAccountSnapshot(t *testing.T) {
|
||||
func TestCachedContactStoreInvalidatesAccountSnapshotAfterMutation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := memory.NewContactStore()
|
||||
if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Alice"}); err != nil {
|
||||
|
|
@ -264,7 +735,123 @@ func TestCachedContactStoreInvalidatesAccountSnapshot(t *testing.T) {
|
|||
t.Fatalf("second = %+v, want Alicia after invalidation", second[2])
|
||||
}
|
||||
if counting.listCalls != 2 {
|
||||
t.Fatalf("ListByUser calls = %d, want 2 after write invalidation", counting.listCalls)
|
||||
t.Fatalf("ListByUser calls = %d, want 2 after safe invalidation and reload", counting.listCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStorePublishedSnapshotsStayImmutableDuringMutations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(context.Context, *CachedContactStore) error
|
||||
}{
|
||||
{
|
||||
name: "upsert",
|
||||
mutate: func(ctx context.Context, cached *CachedContactStore) error {
|
||||
_, err := cached.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "After"})
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete",
|
||||
mutate: func(ctx context.Context, cached *CachedContactStore) error {
|
||||
_, err := cached.Delete(ctx, 1, []int64{2})
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "close_friends",
|
||||
mutate: func(ctx context.Context, cached *CachedContactStore) error {
|
||||
_, err := cached.SetCloseFriends(ctx, 1, []int64{2})
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "personal_photo",
|
||||
mutate: func(ctx context.Context, cached *CachedContactStore) error {
|
||||
_, found, err := cached.SetPersonalPhoto(ctx, 1, 2, 9002, 101)
|
||||
if err == nil && !found {
|
||||
return fmt.Errorf("contact not found")
|
||||
}
|
||||
return err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := memory.NewContactStore()
|
||||
if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Before"}); err != nil {
|
||||
t.Fatalf("seed contact: %v", err)
|
||||
}
|
||||
if _, found, err := base.SetPersonalPhoto(ctx, 1, 2, 9001, 100); err != nil || !found {
|
||||
t.Fatalf("seed personal photo: %v found=%v", err, found)
|
||||
}
|
||||
cached := NewCachedContactStore(base, 0)
|
||||
if _, err := cached.GetMany(ctx, 1, []int64{2}); err != nil {
|
||||
t.Fatalf("warm contacts: %v", err)
|
||||
}
|
||||
if _, err := cached.PersonalPhotos(ctx, 1, []int64{2}); err != nil {
|
||||
t.Fatalf("warm personal photos: %v", err)
|
||||
}
|
||||
|
||||
cached.mu.RLock()
|
||||
contactSnap, contactsWarm := cached.contacts[1]
|
||||
photoSnap, photosWarm := cached.personalPhotos[1]
|
||||
cached.mu.RUnlock()
|
||||
if !contactsWarm || !photosWarm {
|
||||
t.Fatal("snapshots were not warm before mutation")
|
||||
}
|
||||
|
||||
started := make(chan struct{})
|
||||
stop := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
signaled := false
|
||||
for {
|
||||
contact := contactSnap.contacts[2]
|
||||
for i := range contactSnap.ordered {
|
||||
_ = contactSnap.ordered[i].User.ID
|
||||
}
|
||||
ref := photoSnap.refs[2]
|
||||
_, _ = contact.FirstName, ref.PhotoID
|
||||
if !signaled {
|
||||
close(started)
|
||||
signaled = true
|
||||
}
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
waitForCacheTestSignal(t, started)
|
||||
if err := tc.mutate(ctx, cached); err != nil {
|
||||
close(stop)
|
||||
<-done
|
||||
t.Fatalf("mutation: %v", err)
|
||||
}
|
||||
close(stop)
|
||||
<-done
|
||||
|
||||
// A snapshot obtained before invalidation remains a valid immutable
|
||||
// value for an in-flight reader; only the outer cache entry is removed.
|
||||
if got := contactSnap.contacts[2]; got.FirstName != "Before" || got.CloseFriend {
|
||||
t.Fatalf("published contact snapshot mutated in place: %+v", got)
|
||||
}
|
||||
if got := photoSnap.refs[2]; got.PhotoID != 9001 {
|
||||
t.Fatalf("published photo snapshot mutated in place: %+v", got)
|
||||
}
|
||||
cached.mu.RLock()
|
||||
_, contactsWarm = cached.contacts[1]
|
||||
_, photosWarm = cached.personalPhotos[1]
|
||||
cached.mu.RUnlock()
|
||||
if contactsWarm || photosWarm {
|
||||
t.Fatalf("mutation left stale snapshots published: contacts=%v photos=%v", contactsWarm, photosWarm)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -363,6 +950,59 @@ func TestCachedContactStoreDoesNotRefillStaleSnapshotAfterInvalidation(t *testin
|
|||
if cachedHit[2].FirstName != "Alicia" {
|
||||
t.Fatalf("cached value after stale load retry = %+v, want Alicia", cachedHit[2])
|
||||
}
|
||||
if calls := blocking.callCount(); calls != 2 {
|
||||
t.Fatalf("ListByUser calls = %d, want stale load plus exact-viewer retry", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreFlushRejectsEveryInFlightRefill(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := memory.NewContactStore()
|
||||
if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Alice"}); err != nil {
|
||||
t.Fatalf("seed contact: %v", err)
|
||||
}
|
||||
first, err := base.ListByUser(ctx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot first contact list: %v", err)
|
||||
}
|
||||
blocking := &blockingFirstListContactStore{
|
||||
ContactStore: base,
|
||||
started: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
first: first,
|
||||
}
|
||||
cached := NewCachedContactStore(blocking, time.Hour)
|
||||
|
||||
type readResult struct {
|
||||
contacts map[int64]domain.Contact
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan readResult, 1)
|
||||
go func() {
|
||||
contacts, err := cached.GetMany(ctx, 1, []int64{2})
|
||||
resultCh <- readResult{contacts: contacts, err: err}
|
||||
}()
|
||||
waitForCacheTestSignal(t, blocking.started)
|
||||
if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Alicia"}); err != nil {
|
||||
t.Fatalf("update contact while first load is blocked: %v", err)
|
||||
}
|
||||
cached.FlushReadModelCache()
|
||||
close(blocking.release)
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
if result.err != nil {
|
||||
t.Fatalf("contact read: %v", result.err)
|
||||
}
|
||||
if got := result.contacts[2].FirstName; got != "Alicia" {
|
||||
t.Fatalf("flush allowed stale refill: got %q", got)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for contact read")
|
||||
}
|
||||
if calls := blocking.callCount(); calls != 2 {
|
||||
t.Fatalf("ListByUser calls = %d, want stale load plus post-flush retry", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreInvalidatesPersonalPhoto(t *testing.T) {
|
||||
|
|
@ -412,7 +1052,10 @@ func TestCachedContactStoreInvalidatesPersonalPhoto(t *testing.T) {
|
|||
t.Fatalf("PersonalPhotos calls after invalidation = %d, want 2", counting.personalPhotoCalls)
|
||||
}
|
||||
if counting.listCalls != 2 {
|
||||
t.Fatalf("ListByUser calls after invalidation = %d, want 2", counting.listCalls)
|
||||
t.Fatalf("ListByUser calls after mutation = %d, want 2 after safe invalidation and reload", counting.listCalls)
|
||||
}
|
||||
if counting.setPersonalPhotoHit != 1 {
|
||||
t.Fatalf("SetPersonalPhoto calls = %d, want 1", counting.setPersonalPhotoHit)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -466,3 +1109,68 @@ func TestCachedContactStoreDoesNotRefillStalePersonalPhotoAfterInvalidation(t *t
|
|||
t.Fatalf("personal photo after concurrent invalidation = %+v, want 9002", result.refs[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContactStoreOlderPersonalPhotoMutationCannotReinsertStalePair(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := memory.NewContactStore()
|
||||
if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Alice"}); err != nil {
|
||||
t.Fatalf("seed contact: %v", err)
|
||||
}
|
||||
if _, found, err := base.SetPersonalPhoto(ctx, 1, 2, 9000, 99); err != nil || !found {
|
||||
t.Fatalf("seed personal photo: %v found=%v", err, found)
|
||||
}
|
||||
inner := &stalePersonalPhotoWritebackStore{
|
||||
ContactStore: base,
|
||||
started: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
}
|
||||
cached := NewCachedContactStore(inner, 0)
|
||||
if refs, err := cached.PersonalPhotos(ctx, 1, []int64{2}); err != nil || refs[2].PhotoID != 9000 {
|
||||
t.Fatalf("warm personal photo = %+v err=%v, want 9000", refs[2], err)
|
||||
}
|
||||
|
||||
olderCtx := context.WithValue(ctx, stalePersonalPhotoWritebackContextKey{}, true)
|
||||
type setResult struct {
|
||||
found bool
|
||||
err error
|
||||
}
|
||||
olderResult := make(chan setResult, 1)
|
||||
go func() {
|
||||
_, found, err := cached.SetPersonalPhoto(olderCtx, 1, 2, 9001, 100)
|
||||
olderResult <- setResult{found: found, err: err}
|
||||
}()
|
||||
waitForCacheTestSignal(t, inner.started)
|
||||
|
||||
// The newer DB commit completes and invalidates the warm snapshot first.
|
||||
if _, found, err := cached.SetPersonalPhoto(ctx, 1, 2, 9002, 101); err != nil || !found {
|
||||
t.Fatalf("newer personal photo mutation: %v found=%v", err, found)
|
||||
}
|
||||
close(inner.release)
|
||||
select {
|
||||
case result := <-olderResult:
|
||||
if result.err != nil || !result.found {
|
||||
t.Fatalf("older personal photo mutation: %v found=%v", result.err, result.found)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for older personal photo mutation")
|
||||
}
|
||||
|
||||
if calls := inner.staleReads(); calls != 0 {
|
||||
t.Fatalf("post-commit stale PersonalPhotos reads = %d, want 0", calls)
|
||||
}
|
||||
cached.mu.RLock()
|
||||
_, contactsWarm := cached.contacts[1]
|
||||
_, photosWarm := cached.personalPhotos[1]
|
||||
_, pairWarm := cached.projection[contactProjectionKey{viewerUserID: 1, contactUserID: 2}]
|
||||
cached.mu.RUnlock()
|
||||
if contactsWarm || photosWarm || pairWarm {
|
||||
t.Fatalf("older mutation reinserted stale cache state: contacts=%v photos=%v pair=%v", contactsWarm, photosWarm, pairWarm)
|
||||
}
|
||||
refs, err := cached.PersonalPhotos(ctx, 1, []int64{2})
|
||||
if err != nil {
|
||||
t.Fatalf("reload current personal photo: %v", err)
|
||||
}
|
||||
if got := refs[2].PhotoID; got != 9002 {
|
||||
t.Fatalf("personal photo after out-of-order completions = %d, want 9002", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue