owpengram-server/internal/app/userprojection/contact_cache_sparse.go
2026-09-01 12:06:31 +03:00

174 lines
5.6 KiB
Go

package userprojection
import (
"context"
"fmt"
"sort"
"telesrv/internal/domain"
"telesrv/internal/store"
)
var _ store.SparseContactProjectionStore = (*CachedContactStore)(nil)
// ContactProjectionForViewerUserIDs keeps the pair cache useful for sparse
// outbox projection without ever broadening a cold read into viewers x targets.
func (c *CachedContactStore) ContactProjectionForViewerUserIDs(ctx context.Context, requested map[int64][]int64) (domain.ContactProjectionBatch, error) {
pairs := canonicalContactProjectionPairs(requested)
if len(pairs) == 0 {
return emptyContactProjectionBatch(), nil
}
for {
out := emptyContactProjectionBatch()
readFence := c.captureCacheFence(sparseContactProjectionFenceIDs(pairs)...)
now := c.now()
cold := make(map[int64][]int64)
for _, pair := range pairs {
contactKnown := false
if snap, ok := c.lookupContactSnapshot(pair.viewerUserID, now); ok {
contactKnown = true
if contact, found := snap.contacts[pair.contactUserID]; found {
putContactProjectionContact(&out, pair.viewerUserID, pair.contactUserID, contact)
} else {
// Personal photos are rows on contacts and cannot exist when the
// viewer has no contact row for this target.
continue
}
}
photoKnown := false
if snap, ok := c.lookupPersonalPhotoSnapshot(pair.viewerUserID, now); ok {
photoKnown = true
if ref, found := snap.refs[pair.contactUserID]; found {
putContactProjectionPersonalPhoto(&out, pair.viewerUserID, pair.contactUserID, ref)
}
}
if contactKnown && photoKnown {
continue
}
if snap, ok := c.lookupContactProjectionPair(pair.viewerUserID, pair.contactUserID, now); ok {
if !contactKnown && snap.contactFound {
putContactProjectionContact(&out, pair.viewerUserID, pair.contactUserID, snap.contact)
}
if !photoKnown && snap.personalPhotoFound {
putContactProjectionPersonalPhoto(&out, pair.viewerUserID, pair.contactUserID, snap.personalPhoto)
}
continue
}
cold[pair.viewerUserID] = append(cold[pair.viewerUserID], pair.contactUserID)
}
if !c.cacheFenceCurrent(readFence) {
if err := ctx.Err(); err != nil {
return domain.ContactProjectionBatch{}, err
}
continue
}
if len(cold) == 0 {
return out, nil
}
loaded, err := c.loadSparseContactProjection(ctx, cold)
if err != nil {
return domain.ContactProjectionBatch{}, err
}
if !c.cacheFenceCurrent(readFence) {
if err := ctx.Err(); err != nil {
return domain.ContactProjectionBatch{}, err
}
continue
}
mergeContactProjectionBatch(&out, loaded)
return out, nil
}
}
type sparseContactProjectionPair struct {
viewerUserID int64
contactUserID int64
}
func canonicalContactProjectionPairs(requested map[int64][]int64) []sparseContactProjectionPair {
seen := make(map[sparseContactProjectionPair]struct{})
for viewerID, ids := range requested {
if viewerID == 0 {
continue
}
for _, id := range ids {
if id != 0 {
seen[sparseContactProjectionPair{viewerUserID: viewerID, contactUserID: id}] = struct{}{}
}
}
}
out := make([]sparseContactProjectionPair, 0, len(seen))
for pair := range seen {
out = append(out, pair)
}
sort.Slice(out, func(i, j int) bool {
if out[i].viewerUserID == out[j].viewerUserID {
return out[i].contactUserID < out[j].contactUserID
}
return out[i].viewerUserID < out[j].viewerUserID
})
return out
}
func (c *CachedContactStore) loadSparseContactProjection(ctx context.Context, requested map[int64][]int64) (domain.ContactProjectionBatch, error) {
pairs := canonicalContactProjectionPairs(requested)
canonical := make(map[int64][]int64)
for _, pair := range pairs {
canonical[pair.viewerUserID] = append(canonical[pair.viewerUserID], pair.contactUserID)
}
sfKey := fmt.Sprintf("contact-projection-sparse:%v", pairs)
for {
v, err, _ := c.sf.Do(sfKey, func() (any, error) {
loader, ok := c.inner.(store.SparseContactProjectionStore)
if !ok {
return contactProjectionLoadResult{}, fmt.Errorf("contact store does not support sparse projection")
}
loadFence := c.captureCacheFence(sparseContactProjectionFenceIDs(pairs)...)
batch, err := loader.ContactProjectionForViewerUserIDs(ctx, canonical)
if err != nil {
return contactProjectionLoadResult{}, err
}
expireAt := c.now().Add(c.ttl)
admitPairs := len(pairs) <= contactProjectionDenseAdmissionMaxCells
c.mu.Lock()
current := c.cacheFenceCurrentLocked(loadFence)
if current && admitPairs {
for _, pair := range pairs {
contact, contactFound := batch.Contacts[pair.viewerUserID][pair.contactUserID]
ref, photoFound := batch.PersonalPhotos[pair.viewerUserID][pair.contactUserID]
c.storeContactProjectionPairLocked(
contactProjectionKey{viewerUserID: pair.viewerUserID, contactUserID: pair.contactUserID},
contact, contactFound, ref, photoFound, expireAt,
)
}
}
c.mu.Unlock()
return contactProjectionLoadResult{batch: cloneContactProjectionBatch(batch), current: current}, nil
})
if err != nil {
return domain.ContactProjectionBatch{}, err
}
result := v.(contactProjectionLoadResult)
if result.current {
return result.batch, nil
}
if err := ctx.Err(); err != nil {
return domain.ContactProjectionBatch{}, err
}
}
}
func sparseContactProjectionFenceIDs(pairs []sparseContactProjectionPair) []int64 {
ids := make([]int64, 0, len(pairs)*2)
for _, pair := range pairs {
ids = append(ids, pair.viewerUserID, pair.contactUserID)
}
return ids
}
func emptyContactProjectionBatch() domain.ContactProjectionBatch {
return domain.ContactProjectionBatch{
Contacts: map[int64]map[int64]domain.Contact{},
PersonalPhotos: map[int64]map[int64]domain.ProfilePhotoRef{},
}
}