package userprojection import ( "context" "fmt" "reflect" "sync" "testing" "time" "unsafe" "telesrv/internal/domain" "telesrv/internal/store" "telesrv/internal/store/memory" ) type blockingFirstListContactStore struct { store.ContactStore started chan struct{} release chan struct{} first domain.ContactList mu sync.Mutex firstUsed bool listCalls int } type blockingFirstPersonalPhotoStore struct { store.ContactStore started chan struct{} release chan struct{} first map[int64]domain.ProfilePhotoRef mu sync.Mutex 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() close(s.started) select { case <-s.release: case <-ctx.Done(): return domain.ContactList{}, ctx.Err() } return domain.ContactList{Contacts: cloneCachedContacts(s.first.Contacts), Hash: s.first.Hash}, nil } s.mu.Unlock() 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 { s.firstUsed = true first := cloneCachedProfilePhotoRefs(s.first) s.mu.Unlock() close(s.started) select { case <-s.release: case <-ctx.Done(): return nil, ctx.Err() } return first, nil } s.mu.Unlock() return s.ContactStore.PersonalPhotos(ctx, userID, contactUserIDs) } func (s *blockingFirstPersonalPhotoStore) SetPersonalPhoto(ctx context.Context, userID, contactUserID int64, photoID int64, date int) (domain.Contact, bool, error) { return s.ContactStore.SetPersonalPhoto(ctx, userID, contactUserID, photoID, date) } func waitForCacheTestSignal(t *testing.T, ch <-chan struct{}) { t.Helper() select { case <-ch: case <-time.After(2 * time.Second): t.Fatal("timed out waiting for cache test signal") } } type countingContactStore struct { store.ContactStore listCalls int getManyCalls int reverseCalls int projectionCalls int personalPhotoCalls int setPersonalPhotoHit int } func (s *countingContactStore) ListByUser(ctx context.Context, userID int64) (domain.ContactList, error) { s.listCalls++ return s.ContactStore.ListByUser(ctx, userID) } func (s *countingContactStore) GetMany(ctx context.Context, userID int64, contactUserIDs []int64) (map[int64]domain.Contact, error) { s.getManyCalls++ return s.ContactStore.GetMany(ctx, userID, contactUserIDs) } func (s *countingContactStore) GetReverseContacts(ctx context.Context, userID int64, ownerUserIDs []int64) (map[int64]domain.Contact, error) { s.reverseCalls++ 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) } func (s *countingContactStore) SetPersonalPhoto(ctx context.Context, userID, contactUserID int64, photoID int64, date int) (domain.Contact, bool, error) { s.setPersonalPhotoHit++ return s.ContactStore.SetPersonalPhoto(ctx, userID, contactUserID, photoID, date) } func TestCachedContactStoreCachesProjectionReads(t *testing.T) { ctx := context.Background() base := memory.NewContactStore() if _, err := base.Upsert(ctx, 1, domain.ContactInput{ ContactUserID: 2, FirstName: "Alice", Phone: "111", Note: "private note", NoteEntities: []domain.MessageEntity{{Type: domain.MessageEntityBold, Offset: 0, Length: 7}}, }); err != nil { t.Fatalf("upsert contact: %v", err) } counting := &countingContactStore{ContactStore: base} cached := NewCachedContactStore(counting, 0) first, err := cached.GetMany(ctx, 1, []int64{2, 3}) if err != nil { t.Fatalf("get many first: %v", err) } if first[2].FirstName != "Alice" || first[2].Note != "private note" || len(first[2].NoteEntities) != 1 { t.Fatalf("first contact = %+v, want Alice with private note", first[2]) } first[2].NoteEntities[0].Length = 99 second, err := cached.GetMany(ctx, 1, []int64{2, 3}) if err != nil { t.Fatalf("get many second: %v", err) } if second[2].FirstName != "Alice" || second[2].Note != "private note" || len(second[2].NoteEntities) != 1 || second[2].NoteEntities[0].Length != 7 { t.Fatalf("second contact = %+v, want isolated cached Alice note", second[2]) } if counting.listCalls != 1 { t.Fatalf("ListByUser calls = %d, want 1 account snapshot load", counting.listCalls) } if counting.getManyCalls != 0 { t.Fatalf("GetMany calls = %d, want 0 with account snapshot", counting.getManyCalls) } reverse, err := cached.GetReverseContacts(ctx, 2, []int64{1}) if err != nil { t.Fatalf("get reverse: %v", err) } if reverse[1].FirstName != "Alice" { t.Fatalf("reverse contact = %+v, want Alice", reverse[1]) } if counting.reverseCalls != 0 { t.Fatalf("GetReverseContacts calls = %d, want 0 from shared contact cache", counting.reverseCalls) } } 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() owners := make([]int64, 32) for i := range owners { owners[i] = int64(i + 1) if i%2 == 0 { if _, err := base.Upsert(ctx, owners[i], domain.ContactInput{ ContactUserID: 9001, FirstName: "Viewer", }); err != nil { t.Fatalf("seed owner %d: %v", owners[i], err) } } } counting := &countingContactStore{ContactStore: base} cached := NewCachedContactStore(counting, 0) first, err := cached.GetReverseContacts(ctx, 9001, owners) if err != nil { t.Fatalf("first reverse lookup: %v", err) } if len(first) != 16 || counting.reverseCalls != 1 || counting.listCalls != 0 { t.Fatalf("first reverse hits=%d reverseCalls=%d listCalls=%d, want 16/1/0", len(first), counting.reverseCalls, counting.listCalls) } second, err := cached.GetReverseContacts(ctx, 9001, owners) if err != nil { t.Fatalf("second reverse lookup: %v", err) } if len(second) != 16 || counting.reverseCalls != 1 || counting.listCalls != 0 { t.Fatalf("cached reverse hits=%d reverseCalls=%d listCalls=%d, want 16/1/0", len(second), counting.reverseCalls, counting.listCalls) } cached.InvalidateViewers(owners[0]) third, err := cached.GetReverseContacts(ctx, 9001, owners) if err != nil { t.Fatalf("reverse lookup after owner invalidation: %v", err) } if len(third) != 16 || counting.reverseCalls != 2 { t.Fatalf("invalidated reverse hits=%d reverseCalls=%d, want 16/2", len(third), counting.reverseCalls) } } func TestCachedContactStoreReversePairsUsePerEntryLRU(t *testing.T) { ctx := context.Background() base := memory.NewContactStore() for ownerID := int64(1); ownerID <= 3; ownerID++ { if _, err := base.Upsert(ctx, ownerID, domain.ContactInput{ ContactUserID: 9001, FirstName: "Viewer", }); err != nil { t.Fatalf("seed owner %d: %v", ownerID, err) } } counting := &countingContactStore{ContactStore: base} cached := NewCachedContactStore(counting, 0) cached.reverseCap = 2 for _, ownerID := range []int64{1, 2, 1, 3, 1, 2} { got, err := cached.GetReverseContacts(ctx, 9001, []int64{ownerID}) if err != nil { t.Fatalf("reverse owner %d: %v", ownerID, err) } if _, ok := got[ownerID]; !ok { t.Fatalf("reverse owner %d missing", ownerID) } } if counting.reverseCalls != 4 { t.Fatalf("reverse calls = %d, want 4 (owner 1 touched, owner 2 evicted only)", counting.reverseCalls) } if len(cached.reverse) != 2 || cached.reverseLRU.Len() != 2 { t.Fatalf("reverse cache map/list = %d/%d, want 2/2", len(cached.reverse), cached.reverseLRU.Len()) } } 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 { t.Fatalf("upsert contact: %v", err) } counting := &countingContactStore{ContactStore: base} cached := NewCachedContactStore(counting, 0) first, err := cached.GetMany(ctx, 1, []int64{2}) if err != nil { t.Fatalf("get first: %v", err) } if first[2].FirstName != "Alice" { t.Fatalf("first = %+v, want Alice", first[2]) } if _, err := cached.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Alicia"}); err != nil { t.Fatalf("upsert through cache: %v", err) } second, err := cached.GetMany(ctx, 1, []int64{2}) if err != nil { t.Fatalf("get second: %v", err) } if second[2].FirstName != "Alicia" { t.Fatalf("second = %+v, want Alicia after invalidation", second[2]) } if counting.listCalls != 2 { 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) } }) } } func TestCachedContactStoreExternalInvalidationAndFlush(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("upsert contact: %v", err) } counting := &countingContactStore{ContactStore: base} cached := NewCachedContactStore(counting, 0) if _, err := cached.GetMany(ctx, 1, []int64{2}); err != nil { t.Fatalf("prime get: %v", err) } if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Alicia"}); err != nil { t.Fatalf("direct upsert: %v", err) } cached.InvalidateViewers(1) got, err := cached.GetMany(ctx, 1, []int64{2}) if err != nil { t.Fatalf("get after external invalidation: %v", err) } if got[2].FirstName != "Alicia" { t.Fatalf("after invalidation = %+v, want Alicia", got[2]) } if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Ally"}); err != nil { t.Fatalf("direct upsert 2: %v", err) } cached.FlushReadModelCache() got, err = cached.GetMany(ctx, 1, []int64{2}) if err != nil { t.Fatalf("get after flush: %v", err) } if got[2].FirstName != "Ally" { t.Fatalf("after flush = %+v, want Ally", got[2]) } if counting.listCalls != 3 { t.Fatalf("ListByUser calls = %d, want 3 after prime+invalidate+flush", counting.listCalls) } } func TestCachedContactStoreDoesNotRefillStaleSnapshotAfterInvalidation(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, 0) type readResult struct { contacts map[int64]domain.Contact err error } resultCh := make(chan readResult, 1) go func() { got, err := cached.GetMany(ctx, 1, []int64{2}) resultCh <- readResult{contacts: got, 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.InvalidateViewers(1) close(blocking.release) var result readResult select { case result = <-resultCh: case <-time.After(2 * time.Second): t.Fatal("timed out waiting for contact read") } if result.err != nil { t.Fatalf("contact read: %v", result.err) } if result.contacts[2].FirstName != "Alicia" { t.Fatalf("contact after concurrent invalidation = %+v, want Alicia", result.contacts[2]) } cachedHit, err := cached.GetMany(ctx, 1, []int64{2}) if err != nil { t.Fatalf("cached hit after stale load retry: %v", err) } 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) { ctx := context.Background() base := memory.NewContactStore() if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Alice"}); err != nil { t.Fatalf("upsert contact: %v", err) } if _, found, err := base.SetPersonalPhoto(ctx, 1, 2, 9001, 100); err != nil || !found { t.Fatalf("set personal photo: %v found=%v", err, found) } counting := &countingContactStore{ContactStore: base} cached := NewCachedContactStore(counting, 0) first, err := cached.PersonalPhotos(ctx, 1, []int64{2}) if err != nil { t.Fatalf("personal photos first: %v", err) } if first[2].PhotoID != 9001 || !first[2].Personal { t.Fatalf("first personal photo = %+v, want 9001", first[2]) } second, err := cached.PersonalPhotos(ctx, 1, []int64{2}) if err != nil { t.Fatalf("personal photos second: %v", err) } if second[2].PhotoID != 9001 { t.Fatalf("second personal photo = %+v, want 9001", second[2]) } if counting.listCalls != 1 { t.Fatalf("ListByUser calls = %d, want 1 personal-photo account snapshot load", counting.listCalls) } if counting.personalPhotoCalls != 1 { t.Fatalf("PersonalPhotos calls = %d, want 1", counting.personalPhotoCalls) } if _, found, err := cached.SetPersonalPhoto(ctx, 1, 2, 9002, 101); err != nil || !found { t.Fatalf("cached set personal photo: %v found=%v", err, found) } third, err := cached.PersonalPhotos(ctx, 1, []int64{2}) if err != nil { t.Fatalf("personal photos third: %v", err) } if third[2].PhotoID != 9002 { t.Fatalf("third personal photo = %+v, want 9002 after invalidation", third[2]) } if counting.personalPhotoCalls != 2 { t.Fatalf("PersonalPhotos calls after invalidation = %d, want 2", counting.personalPhotoCalls) } if counting.listCalls != 2 { 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) } } func TestCachedContactStoreDoesNotRefillStalePersonalPhotoAfterInvalidation(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("upsert 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) } first, err := base.PersonalPhotos(ctx, 1, []int64{2}) if err != nil { t.Fatalf("snapshot first personal photo: %v", err) } blocking := &blockingFirstPersonalPhotoStore{ ContactStore: base, started: make(chan struct{}), release: make(chan struct{}), first: first, } cached := NewCachedContactStore(blocking, 0) type readResult struct { refs map[int64]domain.ProfilePhotoRef err error } resultCh := make(chan readResult, 1) go func() { refs, err := cached.PersonalPhotos(ctx, 1, []int64{2}) resultCh <- readResult{refs: refs, err: err} }() waitForCacheTestSignal(t, blocking.started) if _, found, err := cached.SetPersonalPhoto(ctx, 1, 2, 9002, 101); err != nil || !found { t.Fatalf("update personal photo while first load is blocked: %v found=%v", err, found) } close(blocking.release) var result readResult select { case result = <-resultCh: case <-time.After(2 * time.Second): t.Fatal("timed out waiting for personal photo read") } if result.err != nil { t.Fatalf("personal photo read: %v", result.err) } if result.refs[2].PhotoID != 9002 { 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) } }