merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
|
|
@ -19,12 +19,21 @@ const (
|
|||
// Normal correctness relies on write-path invalidation, not natural expiry.
|
||||
DefaultContactProjectionCacheTTL = 24 * time.Hour
|
||||
|
||||
contactSnapshotMaxViewers = 4096
|
||||
contactReversePairMaxEntries = 262144
|
||||
contactPersonalPhotoSnapshotCap = 4096
|
||||
// DefaultContactSnapshotMaxViewers covers the 10k online target plus bounded
|
||||
// reconnect overlap. Eviction is exact LRU; reaching the limit must never
|
||||
// clear every viewer snapshot at once.
|
||||
DefaultContactSnapshotMaxViewers = 16_384
|
||||
contactReversePairMaxEntries = 262144
|
||||
contactProjectionPairMaxEntries = 262144
|
||||
// One dense request must not monopolize the pair LRU or hold the global
|
||||
// cache lock while inserting and evicting hundreds of thousands of cells.
|
||||
// Larger results are still returned; they simply are not admitted per pair.
|
||||
contactProjectionDenseAdmissionMaxCells = contactProjectionPairMaxEntries / 16
|
||||
)
|
||||
|
||||
type contactAccountSnapshot struct {
|
||||
// contacts and ordered are immutable after the snapshot is published in
|
||||
// CachedContactStore.contacts. Readers intentionally retain shallow copies.
|
||||
contacts map[int64]domain.Contact
|
||||
ordered []domain.Contact
|
||||
hash int64
|
||||
|
|
@ -32,6 +41,7 @@ type contactAccountSnapshot struct {
|
|||
}
|
||||
|
||||
type personalPhotoSnapshot struct {
|
||||
// refs is immutable after the snapshot is published in personalPhotos.
|
||||
refs map[int64]domain.ProfilePhotoRef
|
||||
expireAt time.Time
|
||||
}
|
||||
|
|
@ -42,8 +52,8 @@ type reverseContactKey struct {
|
|||
}
|
||||
|
||||
type reverseContactSnapshot struct {
|
||||
contact domain.Contact
|
||||
found bool
|
||||
// contact is an immutable cached clone. nil is the negative-cache value.
|
||||
contact *domain.Contact
|
||||
expireAt time.Time
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +62,79 @@ type reverseContactEntry struct {
|
|||
snapshot reverseContactSnapshot
|
||||
}
|
||||
|
||||
type contactProjectionKey struct {
|
||||
viewerUserID int64
|
||||
contactUserID int64
|
||||
}
|
||||
|
||||
// cachedContactProjectionOverlay is the viewer-owned part of a contact row.
|
||||
// Base user data is loaded and cached independently, so retaining domain.User
|
||||
// here would multiply a large, viewer-independent value across every pair.
|
||||
// Values are immutable after publication; noteEntities is cloned on both sides
|
||||
// of the cache boundary.
|
||||
type cachedContactProjectionOverlay struct {
|
||||
firstName string
|
||||
lastName string
|
||||
phone string
|
||||
note string
|
||||
noteEntities []domain.MessageEntity
|
||||
mutual bool
|
||||
closeFriend bool
|
||||
}
|
||||
|
||||
func newCachedContactProjectionOverlay(contact domain.Contact) *cachedContactProjectionOverlay {
|
||||
return &cachedContactProjectionOverlay{
|
||||
firstName: contact.FirstName,
|
||||
lastName: contact.LastName,
|
||||
phone: contact.Phone,
|
||||
note: contact.Note,
|
||||
noteEntities: append([]domain.MessageEntity(nil), contact.NoteEntities...),
|
||||
mutual: contact.Mutual || contact.User.Mutual,
|
||||
closeFriend: contact.CloseFriend || contact.User.CloseFriend,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *cachedContactProjectionOverlay) domainContact(contactUserID int64) domain.Contact {
|
||||
if o == nil {
|
||||
return domain.Contact{}
|
||||
}
|
||||
return domain.Contact{
|
||||
User: domain.User{ID: contactUserID},
|
||||
FirstName: o.firstName,
|
||||
LastName: o.lastName,
|
||||
Phone: o.phone,
|
||||
Note: o.note,
|
||||
NoteEntities: append([]domain.MessageEntity(nil), o.noteEntities...),
|
||||
Mutual: o.mutual,
|
||||
CloseFriend: o.closeFriend,
|
||||
}
|
||||
}
|
||||
|
||||
type contactProjectionSnapshot struct {
|
||||
// Positive values point at immutable cached clones; nil is negative. Keeping
|
||||
// the compact viewer-owned overlay outside the entry makes negative pairs
|
||||
// consume only two pointers plus their expiry and avoids duplicating a full
|
||||
// base User for every positive pair.
|
||||
contact *cachedContactProjectionOverlay
|
||||
personalPhoto *domain.ProfilePhotoRef
|
||||
expireAt time.Time
|
||||
}
|
||||
|
||||
// contactProjectionLookup is a transient, caller-owned copy. It deliberately
|
||||
// retains the old value+found shape so no mutable slice from a cached pointer is
|
||||
// exposed after the cache lock is released.
|
||||
type contactProjectionLookup struct {
|
||||
contact domain.Contact
|
||||
contactFound bool
|
||||
personalPhoto domain.ProfilePhotoRef
|
||||
personalPhotoFound bool
|
||||
}
|
||||
|
||||
type contactProjectionEntry struct {
|
||||
key contactProjectionKey
|
||||
snapshot contactProjectionSnapshot
|
||||
}
|
||||
|
||||
type contactSnapshotLoadResult struct {
|
||||
snap contactAccountSnapshot
|
||||
stored bool
|
||||
|
|
@ -67,6 +150,21 @@ type personalPhotoSnapshotLoadResult struct {
|
|||
stored bool
|
||||
}
|
||||
|
||||
type contactProjectionLoadResult struct {
|
||||
batch domain.ContactProjectionBatch
|
||||
current bool
|
||||
}
|
||||
|
||||
type contactCacheViewerFence struct {
|
||||
userID int64
|
||||
generation uint64
|
||||
}
|
||||
|
||||
type contactCacheFence struct {
|
||||
flushGeneration uint64
|
||||
viewers []contactCacheViewerFence
|
||||
}
|
||||
|
||||
// CachedContactStore wraps ContactStore with account-level read model snapshots.
|
||||
//
|
||||
// Contact data is low-churn and high-read: TDesktop repeatedly asks for the same
|
||||
|
|
@ -79,34 +177,65 @@ type CachedContactStore struct {
|
|||
ttl time.Duration
|
||||
now func() time.Time
|
||||
|
||||
mu sync.RWMutex
|
||||
contacts map[int64]contactAccountSnapshot
|
||||
personalPhotos map[int64]personalPhotoSnapshot
|
||||
reverse map[reverseContactKey]*list.Element
|
||||
reverseLRU *list.List
|
||||
reverseByOwner map[int64]map[int64]struct{}
|
||||
reverseCap int
|
||||
epoch uint64
|
||||
sf singleflight.Group
|
||||
mu sync.RWMutex
|
||||
contacts map[int64]contactAccountSnapshot
|
||||
contactLRU *list.List
|
||||
contactElements map[int64]*list.Element
|
||||
contactCap int
|
||||
personalPhotos map[int64]personalPhotoSnapshot
|
||||
personalPhotoLRU *list.List
|
||||
personalElements map[int64]*list.Element
|
||||
personalPhotoCap int
|
||||
reverse map[reverseContactKey]*list.Element
|
||||
reverseLRU *list.List
|
||||
reverseByOwner map[int64]map[int64]struct{}
|
||||
reverseCap int
|
||||
projection map[contactProjectionKey]*list.Element
|
||||
projectionLRU *list.List
|
||||
projectionByViewer map[int64]map[int64]struct{}
|
||||
projectionByTarget map[int64]map[int64]struct{}
|
||||
projectionCap int
|
||||
flushGeneration uint64
|
||||
viewerGenerations map[int64]uint64
|
||||
sf singleflight.Group
|
||||
}
|
||||
|
||||
func NewCachedContactStore(inner store.ContactStore, ttl time.Duration) *CachedContactStore {
|
||||
return NewCachedContactStoreWithMaxViewers(inner, ttl, DefaultContactSnapshotMaxViewers)
|
||||
}
|
||||
|
||||
func NewCachedContactStoreWithMaxViewers(inner store.ContactStore, ttl time.Duration, maxViewers int) *CachedContactStore {
|
||||
if inner == nil {
|
||||
return nil
|
||||
}
|
||||
if ttl <= 0 {
|
||||
ttl = DefaultContactProjectionCacheTTL
|
||||
}
|
||||
if maxViewers <= 0 {
|
||||
maxViewers = DefaultContactSnapshotMaxViewers
|
||||
}
|
||||
return &CachedContactStore{
|
||||
inner: inner,
|
||||
ttl: ttl,
|
||||
now: time.Now,
|
||||
contacts: make(map[int64]contactAccountSnapshot, 1024),
|
||||
personalPhotos: make(map[int64]personalPhotoSnapshot, 1024),
|
||||
reverse: make(map[reverseContactKey]*list.Element, 4096),
|
||||
reverseLRU: list.New(),
|
||||
reverseByOwner: make(map[int64]map[int64]struct{}, 1024),
|
||||
reverseCap: contactReversePairMaxEntries,
|
||||
inner: inner,
|
||||
ttl: ttl,
|
||||
now: time.Now,
|
||||
contacts: make(map[int64]contactAccountSnapshot, 1024),
|
||||
contactLRU: list.New(),
|
||||
contactElements: make(map[int64]*list.Element, 1024),
|
||||
contactCap: maxViewers,
|
||||
personalPhotos: make(map[int64]personalPhotoSnapshot, 1024),
|
||||
personalPhotoLRU: list.New(),
|
||||
personalElements: make(map[int64]*list.Element, 1024),
|
||||
personalPhotoCap: maxViewers,
|
||||
reverse: make(map[reverseContactKey]*list.Element, 4096),
|
||||
reverseLRU: list.New(),
|
||||
reverseByOwner: make(map[int64]map[int64]struct{}, 1024),
|
||||
reverseCap: contactReversePairMaxEntries,
|
||||
projection: make(map[contactProjectionKey]*list.Element, 4096),
|
||||
projectionLRU: list.New(),
|
||||
projectionByViewer: make(map[int64]map[int64]struct{}, 1024),
|
||||
projectionByTarget: make(map[int64]map[int64]struct{}, 1024),
|
||||
projectionCap: contactProjectionPairMaxEntries,
|
||||
viewerGenerations: make(map[int64]uint64, 1024),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,6 +326,157 @@ func (c *CachedContactStore) GetReverseContacts(ctx context.Context, userID int6
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) ContactProjectionForViewers(ctx context.Context, viewerUserIDs, contactUserIDs []int64) (domain.ContactProjectionBatch, error) {
|
||||
viewers := dedupContactIDs(viewerUserIDs)
|
||||
targets := dedupContactIDs(contactUserIDs)
|
||||
if len(viewers) == 0 || len(targets) == 0 {
|
||||
return domain.ContactProjectionBatch{
|
||||
Contacts: map[int64]map[int64]domain.Contact{},
|
||||
PersonalPhotos: map[int64]map[int64]domain.ProfilePhotoRef{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
for {
|
||||
out := domain.ContactProjectionBatch{
|
||||
Contacts: make(map[int64]map[int64]domain.Contact, len(viewers)),
|
||||
PersonalPhotos: make(map[int64]map[int64]domain.ProfilePhotoRef, len(viewers)),
|
||||
}
|
||||
readFence := c.captureCacheFenceSlices(viewers, targets)
|
||||
now := c.now()
|
||||
coldViewers := make(map[int64]struct{}, len(viewers))
|
||||
coldTargets := make(map[int64]struct{}, len(targets))
|
||||
for _, viewerID := range viewers {
|
||||
var contactSnap contactAccountSnapshot
|
||||
contactsWarm := false
|
||||
if snap, ok := c.lookupContactSnapshot(viewerID, now); ok {
|
||||
contactsWarm = true
|
||||
contactSnap = snap
|
||||
for _, targetID := range targets {
|
||||
if contact, found := snap.contacts[targetID]; found {
|
||||
putContactProjectionContact(&out, viewerID, targetID, contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
personalPhotosWarm := false
|
||||
if snap, ok := c.lookupPersonalPhotoSnapshot(viewerID, now); ok {
|
||||
personalPhotosWarm = true
|
||||
for _, targetID := range targets {
|
||||
if ref, found := snap.refs[targetID]; found {
|
||||
putContactProjectionPersonalPhoto(&out, viewerID, targetID, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, targetID := range targets {
|
||||
if contactsWarm && personalPhotosWarm {
|
||||
continue
|
||||
}
|
||||
if contactsWarm {
|
||||
if _, found := contactSnap.contacts[targetID]; !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if snap, ok := c.lookupContactProjectionPair(viewerID, targetID, now); ok {
|
||||
if !contactsWarm && snap.contactFound {
|
||||
putContactProjectionContact(&out, viewerID, targetID, snap.contact)
|
||||
}
|
||||
if !personalPhotosWarm && snap.personalPhotoFound {
|
||||
putContactProjectionPersonalPhoto(&out, viewerID, targetID, snap.personalPhoto)
|
||||
}
|
||||
continue
|
||||
}
|
||||
coldViewers[viewerID] = struct{}{}
|
||||
coldTargets[targetID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if !c.cacheFenceCurrent(readFence) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return domain.ContactProjectionBatch{}, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(coldViewers) == 0 || len(coldTargets) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
cold := make([]int64, 0, len(coldViewers))
|
||||
for viewerID := range coldViewers {
|
||||
cold = append(cold, viewerID)
|
||||
}
|
||||
coldIDs := make([]int64, 0, len(coldTargets))
|
||||
for targetID := range coldTargets {
|
||||
coldIDs = append(coldIDs, targetID)
|
||||
}
|
||||
loaded, err := c.loadContactProjectionForViewers(ctx, cold, coldIDs)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) loadContactProjectionForViewers(ctx context.Context, viewerUserIDs, contactUserIDs []int64) (domain.ContactProjectionBatch, error) {
|
||||
viewers := append([]int64(nil), viewerUserIDs...)
|
||||
targets := append([]int64(nil), contactUserIDs...)
|
||||
sort.Slice(viewers, func(i, j int) bool { return viewers[i] < viewers[j] })
|
||||
sort.Slice(targets, func(i, j int) bool { return targets[i] < targets[j] })
|
||||
sfKey := fmt.Sprintf("contact-projection:%v:%v", viewers, targets)
|
||||
for {
|
||||
v, err, _ := c.sf.Do(sfKey, func() (any, error) {
|
||||
loadFence := c.captureCacheFenceSlices(viewers, targets)
|
||||
batch, err := c.inner.ContactProjectionForViewers(ctx, viewers, targets)
|
||||
if err != nil {
|
||||
return contactProjectionLoadResult{}, err
|
||||
}
|
||||
now := c.now()
|
||||
expireAt := now.Add(c.ttl)
|
||||
admitPairs := admitDenseContactProjectionPairs(len(viewers), len(targets))
|
||||
c.mu.Lock()
|
||||
current := c.cacheFenceCurrentLocked(loadFence)
|
||||
if current && admitPairs {
|
||||
for _, viewerID := range viewers {
|
||||
for _, targetID := range targets {
|
||||
contact, contactFound := batch.Contacts[viewerID][targetID]
|
||||
ref, personalPhotoFound := batch.PersonalPhotos[viewerID][targetID]
|
||||
c.storeContactProjectionPairLocked(
|
||||
contactProjectionKey{viewerUserID: viewerID, contactUserID: targetID},
|
||||
contact, contactFound, ref, personalPhotoFound, 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 admitDenseContactProjectionPairs(viewerCount, targetCount int) bool {
|
||||
if viewerCount <= 0 || targetCount <= 0 || targetCount > contactProjectionDenseAdmissionMaxCells {
|
||||
return false
|
||||
}
|
||||
// Division avoids overflowing int for attacker-controlled vector lengths.
|
||||
return viewerCount <= contactProjectionDenseAdmissionMaxCells/targetCount
|
||||
}
|
||||
|
||||
// loadReverseContacts performs at most one batched cold-store read for all
|
||||
// missing owner→viewer pairs, then caches both hits and misses. Privacy
|
||||
// projection therefore stays memory-only after warm-up instead of repeating a
|
||||
|
|
@ -207,7 +487,7 @@ func (c *CachedContactStore) loadReverseContacts(ctx context.Context, userID int
|
|||
sfKey := fmt.Sprintf("contact-reverse:%d:%v", userID, owners)
|
||||
for {
|
||||
v, err, _ := c.sf.Do(sfKey, func() (any, error) {
|
||||
loadEpoch := c.cacheEpoch()
|
||||
loadFence := c.captureCacheFenceSlices(owners, []int64{userID})
|
||||
contacts, err := c.inner.GetReverseContacts(ctx, userID, owners)
|
||||
if err != nil {
|
||||
return reverseContactLoadResult{}, err
|
||||
|
|
@ -215,16 +495,12 @@ func (c *CachedContactStore) loadReverseContacts(ctx context.Context, userID int
|
|||
now := c.now()
|
||||
expireAt := now.Add(c.ttl)
|
||||
c.mu.Lock()
|
||||
stored := c.epoch == loadEpoch
|
||||
stored := c.cacheFenceCurrentLocked(loadFence)
|
||||
if stored {
|
||||
for _, ownerID := range owners {
|
||||
key := reverseContactKey{ownerUserID: ownerID, contactUserID: userID}
|
||||
contact, found := contacts[ownerID]
|
||||
c.storeReverseContactLocked(key, reverseContactSnapshot{
|
||||
contact: cloneCachedContact(contact),
|
||||
found: found,
|
||||
expireAt: expireAt,
|
||||
})
|
||||
c.storeReverseContactLocked(key, contact, found, expireAt)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
|
@ -249,6 +525,9 @@ func (c *CachedContactStore) loadReverseContacts(ctx context.Context, userID int
|
|||
func (c *CachedContactStore) Upsert(ctx context.Context, userID int64, input domain.ContactInput) (domain.Contact, error) {
|
||||
contact, err := c.inner.Upsert(ctx, userID, input)
|
||||
if err == nil {
|
||||
// Published account snapshots are immutable. Invalidate instead of
|
||||
// modifying their inner maps/slices in place or publishing a mutation
|
||||
// payload whose cache-write order may differ from its DB commit order.
|
||||
c.InvalidateViewers(userID, input.ContactUserID)
|
||||
}
|
||||
return contact, err
|
||||
|
|
@ -257,12 +536,11 @@ func (c *CachedContactStore) Upsert(ctx context.Context, userID int64, input dom
|
|||
func (c *CachedContactStore) UpsertMany(ctx context.Context, userID int64, inputs []domain.ContactInput) ([]domain.Contact, error) {
|
||||
contacts, err := c.inner.UpsertMany(ctx, userID, inputs)
|
||||
if err == nil {
|
||||
ids := make([]int64, 0, len(inputs)+1)
|
||||
ids = append(ids, userID)
|
||||
ids := make([]int64, 0, len(inputs))
|
||||
for _, input := range inputs {
|
||||
ids = append(ids, input.ContactUserID)
|
||||
}
|
||||
c.InvalidateViewers(ids...)
|
||||
c.InvalidateViewers(append([]int64{userID}, ids...)...)
|
||||
}
|
||||
return contacts, err
|
||||
}
|
||||
|
|
@ -270,7 +548,9 @@ func (c *CachedContactStore) UpsertMany(ctx context.Context, userID int64, input
|
|||
func (c *CachedContactStore) UpdateNote(ctx context.Context, userID, contactUserID int64, note string, entities []domain.MessageEntity) (domain.Contact, bool, error) {
|
||||
contact, found, err := c.inner.UpdateNote(ctx, userID, contactUserID, note, entities)
|
||||
if err == nil {
|
||||
c.InvalidateViewers(userID)
|
||||
if found {
|
||||
c.InvalidateViewers(userID)
|
||||
}
|
||||
}
|
||||
return contact, found, err
|
||||
}
|
||||
|
|
@ -285,7 +565,10 @@ func (c *CachedContactStore) SetCloseFriends(ctx context.Context, userID int64,
|
|||
|
||||
func (c *CachedContactStore) SetPersonalPhoto(ctx context.Context, userID, contactUserID int64, photoID int64, date int) (domain.Contact, bool, error) {
|
||||
contact, found, err := c.inner.SetPersonalPhoto(ctx, userID, contactUserID, photoID, date)
|
||||
if err == nil {
|
||||
if err == nil && found {
|
||||
// Do not perform a post-commit read followed by write-through: two
|
||||
// concurrent mutations can complete their cache writes in the opposite
|
||||
// order and reinsert a stale pair after a newer NOTIFY invalidation.
|
||||
c.InvalidateViewers(userID)
|
||||
}
|
||||
return contact, found, err
|
||||
|
|
@ -314,10 +597,7 @@ func (c *CachedContactStore) PersonalPhotos(ctx context.Context, userID int64, c
|
|||
func (c *CachedContactStore) Delete(ctx context.Context, userID int64, contactUserIDs []int64) (int, error) {
|
||||
count, err := c.inner.Delete(ctx, userID, contactUserIDs)
|
||||
if err == nil {
|
||||
ids := make([]int64, 0, len(contactUserIDs)+1)
|
||||
ids = append(ids, userID)
|
||||
ids = append(ids, contactUserIDs...)
|
||||
c.InvalidateViewers(ids...)
|
||||
c.InvalidateViewers(append([]int64{userID}, contactUserIDs...)...)
|
||||
}
|
||||
return count, err
|
||||
}
|
||||
|
|
@ -356,20 +636,16 @@ func (c *CachedContactStore) contactSnapshot(ctx context.Context, userID int64)
|
|||
if snap, ok := c.lookupContactSnapshot(userID, now); ok {
|
||||
return contactSnapshotLoadResult{snap: snap, stored: true}, nil
|
||||
}
|
||||
loadEpoch := c.cacheEpoch()
|
||||
loadFence := c.captureCacheFence(userID)
|
||||
list, err := c.inner.ListByUser(ctx, userID)
|
||||
if err != nil {
|
||||
return contactSnapshotLoadResult{}, err
|
||||
}
|
||||
snap := buildContactAccountSnapshot(list, now.Add(c.ttl))
|
||||
c.mu.Lock()
|
||||
stored := c.epoch == loadEpoch
|
||||
stored := c.cacheFenceCurrentLocked(loadFence)
|
||||
if stored {
|
||||
if len(c.contacts) >= contactSnapshotMaxViewers {
|
||||
c.contacts = make(map[int64]contactAccountSnapshot, 1024)
|
||||
c.personalPhotos = make(map[int64]personalPhotoSnapshot, 1024)
|
||||
}
|
||||
c.contacts[userID] = snap
|
||||
c.storeContactSnapshotLocked(userID, snap)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return contactSnapshotLoadResult{snap: snap, stored: stored}, nil
|
||||
|
|
@ -388,18 +664,45 @@ func (c *CachedContactStore) contactSnapshot(ctx context.Context, userID int64)
|
|||
}
|
||||
|
||||
func (c *CachedContactStore) lookupContactSnapshot(userID int64, now time.Time) (contactAccountSnapshot, bool) {
|
||||
c.mu.RLock()
|
||||
c.mu.Lock()
|
||||
snap, ok := c.contacts[userID]
|
||||
c.mu.RUnlock()
|
||||
if !ok || !snap.expireAt.After(now) {
|
||||
if ok {
|
||||
c.InvalidateViewers(userID)
|
||||
}
|
||||
if !ok {
|
||||
c.mu.Unlock()
|
||||
return contactAccountSnapshot{}, false
|
||||
}
|
||||
if !snap.expireAt.After(now) {
|
||||
c.advanceViewerGenerationLocked(userID)
|
||||
c.invalidateViewerLocked(userID)
|
||||
c.mu.Unlock()
|
||||
return contactAccountSnapshot{}, false
|
||||
}
|
||||
if element := c.contactElements[userID]; element != nil {
|
||||
c.contactLRU.MoveToFront(element)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return snap, true
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) storeContactSnapshotLocked(userID int64, snap contactAccountSnapshot) {
|
||||
if element := c.contactElements[userID]; element != nil {
|
||||
c.contacts[userID] = snap
|
||||
c.contactLRU.MoveToFront(element)
|
||||
return
|
||||
}
|
||||
c.contacts[userID] = snap
|
||||
c.contactElements[userID] = c.contactLRU.PushFront(userID)
|
||||
for c.contactLRU.Len() > c.contactCap {
|
||||
oldest := c.contactLRU.Back()
|
||||
if oldest == nil {
|
||||
break
|
||||
}
|
||||
oldestUserID := oldest.Value.(int64)
|
||||
delete(c.contacts, oldestUserID)
|
||||
delete(c.contactElements, oldestUserID)
|
||||
c.contactLRU.Remove(oldest)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) personalPhotoSnapshot(ctx context.Context, userID int64) (personalPhotoSnapshot, error) {
|
||||
for {
|
||||
if snap, ok := c.lookupPersonalPhotoSnapshot(userID, c.now()); ok {
|
||||
|
|
@ -410,7 +713,7 @@ func (c *CachedContactStore) personalPhotoSnapshot(ctx context.Context, userID i
|
|||
if snap, ok := c.lookupPersonalPhotoSnapshot(userID, now); ok {
|
||||
return personalPhotoSnapshotLoadResult{snap: snap, stored: true}, nil
|
||||
}
|
||||
loadEpoch := c.cacheEpoch()
|
||||
loadFence := c.captureCacheFence(userID)
|
||||
contacts, err := c.contactSnapshot(ctx, userID)
|
||||
if err != nil {
|
||||
return personalPhotoSnapshotLoadResult{}, err
|
||||
|
|
@ -428,12 +731,9 @@ func (c *CachedContactStore) personalPhotoSnapshot(ctx context.Context, userID i
|
|||
}
|
||||
snap := personalPhotoSnapshot{refs: cloneCachedProfilePhotoRefs(refs), expireAt: now.Add(c.ttl)}
|
||||
c.mu.Lock()
|
||||
stored := c.epoch == loadEpoch
|
||||
stored := c.cacheFenceCurrentLocked(loadFence)
|
||||
if stored {
|
||||
if len(c.personalPhotos) >= contactPersonalPhotoSnapshotCap {
|
||||
c.personalPhotos = make(map[int64]personalPhotoSnapshot, 1024)
|
||||
}
|
||||
c.personalPhotos[userID] = snap
|
||||
c.storePersonalPhotoSnapshotLocked(userID, snap)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return personalPhotoSnapshotLoadResult{snap: snap, stored: stored}, nil
|
||||
|
|
@ -452,18 +752,45 @@ func (c *CachedContactStore) personalPhotoSnapshot(ctx context.Context, userID i
|
|||
}
|
||||
|
||||
func (c *CachedContactStore) lookupPersonalPhotoSnapshot(userID int64, now time.Time) (personalPhotoSnapshot, bool) {
|
||||
c.mu.RLock()
|
||||
c.mu.Lock()
|
||||
snap, ok := c.personalPhotos[userID]
|
||||
c.mu.RUnlock()
|
||||
if !ok || !snap.expireAt.After(now) {
|
||||
if ok {
|
||||
c.InvalidateViewers(userID)
|
||||
}
|
||||
if !ok {
|
||||
c.mu.Unlock()
|
||||
return personalPhotoSnapshot{}, false
|
||||
}
|
||||
if !snap.expireAt.After(now) {
|
||||
c.advanceViewerGenerationLocked(userID)
|
||||
c.invalidateViewerLocked(userID)
|
||||
c.mu.Unlock()
|
||||
return personalPhotoSnapshot{}, false
|
||||
}
|
||||
if element := c.personalElements[userID]; element != nil {
|
||||
c.personalPhotoLRU.MoveToFront(element)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return snap, true
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) storePersonalPhotoSnapshotLocked(userID int64, snap personalPhotoSnapshot) {
|
||||
if element := c.personalElements[userID]; element != nil {
|
||||
c.personalPhotos[userID] = snap
|
||||
c.personalPhotoLRU.MoveToFront(element)
|
||||
return
|
||||
}
|
||||
c.personalPhotos[userID] = snap
|
||||
c.personalElements[userID] = c.personalPhotoLRU.PushFront(userID)
|
||||
for c.personalPhotoLRU.Len() > c.personalPhotoCap {
|
||||
oldest := c.personalPhotoLRU.Back()
|
||||
if oldest == nil {
|
||||
break
|
||||
}
|
||||
oldestUserID := oldest.Value.(int64)
|
||||
delete(c.personalPhotos, oldestUserID)
|
||||
delete(c.personalElements, oldestUserID)
|
||||
c.personalPhotoLRU.Remove(oldest)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) lookupReverseContact(ownerUserID, contactUserID int64, now time.Time) (domain.Contact, bool, bool) {
|
||||
key := reverseContactKey{ownerUserID: ownerUserID, contactUserID: contactUserID}
|
||||
c.mu.Lock()
|
||||
|
|
@ -481,10 +808,19 @@ func (c *CachedContactStore) lookupReverseContact(ownerUserID, contactUserID int
|
|||
}
|
||||
c.reverseLRU.MoveToFront(element)
|
||||
c.mu.Unlock()
|
||||
return cloneCachedContact(snap.contact), snap.found, true
|
||||
if snap.contact == nil {
|
||||
return domain.Contact{}, false, true
|
||||
}
|
||||
return cloneCachedContact(*snap.contact), true, true
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) storeReverseContactLocked(key reverseContactKey, snapshot reverseContactSnapshot) {
|
||||
func (c *CachedContactStore) storeReverseContactLocked(key reverseContactKey, contact domain.Contact, found bool, expireAt time.Time) {
|
||||
var cached *domain.Contact
|
||||
if found {
|
||||
clone := cloneCachedContact(contact)
|
||||
cached = &clone
|
||||
}
|
||||
snapshot := reverseContactSnapshot{contact: cached, expireAt: expireAt}
|
||||
if element, ok := c.reverse[key]; ok {
|
||||
entry := element.Value.(*reverseContactEntry)
|
||||
entry.snapshot = snapshot
|
||||
|
|
@ -517,46 +853,222 @@ func (c *CachedContactStore) removeReverseElementLocked(element *list.Element) {
|
|||
c.reverseLRU.Remove(element)
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) lookupContactProjectionPair(viewerUserID, contactUserID int64, now time.Time) (contactProjectionLookup, bool) {
|
||||
key := contactProjectionKey{viewerUserID: viewerUserID, contactUserID: contactUserID}
|
||||
c.mu.Lock()
|
||||
element, ok := c.projection[key]
|
||||
if !ok {
|
||||
c.mu.Unlock()
|
||||
return contactProjectionLookup{}, false
|
||||
}
|
||||
entry := element.Value.(*contactProjectionEntry)
|
||||
snap := entry.snapshot
|
||||
if !snap.expireAt.After(now) {
|
||||
c.removeContactProjectionElementLocked(element)
|
||||
c.mu.Unlock()
|
||||
return contactProjectionLookup{}, false
|
||||
}
|
||||
c.projectionLRU.MoveToFront(element)
|
||||
c.mu.Unlock()
|
||||
result := contactProjectionLookup{}
|
||||
if snap.contact != nil {
|
||||
result.contact = snap.contact.domainContact(contactUserID)
|
||||
result.contactFound = true
|
||||
}
|
||||
if snap.personalPhoto != nil {
|
||||
result.personalPhoto = cloneCachedProfilePhotoRef(*snap.personalPhoto)
|
||||
result.personalPhotoFound = true
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) storeContactProjectionPairLocked(
|
||||
key contactProjectionKey,
|
||||
contact domain.Contact,
|
||||
contactFound bool,
|
||||
personalPhoto domain.ProfilePhotoRef,
|
||||
personalPhotoFound bool,
|
||||
expireAt time.Time,
|
||||
) {
|
||||
var cachedContact *cachedContactProjectionOverlay
|
||||
if contactFound {
|
||||
cachedContact = newCachedContactProjectionOverlay(contact)
|
||||
}
|
||||
var cachedPersonalPhoto *domain.ProfilePhotoRef
|
||||
if personalPhotoFound {
|
||||
clone := cloneCachedProfilePhotoRef(personalPhoto)
|
||||
cachedPersonalPhoto = &clone
|
||||
}
|
||||
snapshot := contactProjectionSnapshot{
|
||||
contact: cachedContact,
|
||||
personalPhoto: cachedPersonalPhoto,
|
||||
expireAt: expireAt,
|
||||
}
|
||||
if element, ok := c.projection[key]; ok {
|
||||
entry := element.Value.(*contactProjectionEntry)
|
||||
entry.snapshot = snapshot
|
||||
c.projectionLRU.MoveToFront(element)
|
||||
return
|
||||
}
|
||||
element := c.projectionLRU.PushFront(&contactProjectionEntry{key: key, snapshot: snapshot})
|
||||
c.projection[key] = element
|
||||
if c.projectionByViewer[key.viewerUserID] == nil {
|
||||
c.projectionByViewer[key.viewerUserID] = make(map[int64]struct{})
|
||||
}
|
||||
c.projectionByViewer[key.viewerUserID][key.contactUserID] = struct{}{}
|
||||
if c.projectionByTarget[key.contactUserID] == nil {
|
||||
c.projectionByTarget[key.contactUserID] = make(map[int64]struct{})
|
||||
}
|
||||
c.projectionByTarget[key.contactUserID][key.viewerUserID] = struct{}{}
|
||||
for c.projectionLRU.Len() > c.projectionCap {
|
||||
c.removeContactProjectionElementLocked(c.projectionLRU.Back())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) removeContactProjectionElementLocked(element *list.Element) {
|
||||
if element == nil {
|
||||
return
|
||||
}
|
||||
entry := element.Value.(*contactProjectionEntry)
|
||||
delete(c.projection, entry.key)
|
||||
if targets := c.projectionByViewer[entry.key.viewerUserID]; targets != nil {
|
||||
delete(targets, entry.key.contactUserID)
|
||||
if len(targets) == 0 {
|
||||
delete(c.projectionByViewer, entry.key.viewerUserID)
|
||||
}
|
||||
}
|
||||
if viewers := c.projectionByTarget[entry.key.contactUserID]; viewers != nil {
|
||||
delete(viewers, entry.key.viewerUserID)
|
||||
if len(viewers) == 0 {
|
||||
delete(c.projectionByTarget, entry.key.contactUserID)
|
||||
}
|
||||
}
|
||||
c.projectionLRU.Remove(element)
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) InvalidateViewers(ids ...int64) {
|
||||
if c == nil || len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.epoch++
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
delete(c.contacts, id)
|
||||
delete(c.personalPhotos, id)
|
||||
for contactUserID := range c.reverseByOwner[id] {
|
||||
if element, ok := c.reverse[reverseContactKey{ownerUserID: id, contactUserID: contactUserID}]; ok {
|
||||
c.removeReverseElementLocked(element)
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
c.advanceViewerGenerationLocked(id)
|
||||
c.invalidateViewerLocked(id)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) invalidateViewerLocked(id int64) {
|
||||
delete(c.contacts, id)
|
||||
if element := c.contactElements[id]; element != nil {
|
||||
delete(c.contactElements, id)
|
||||
c.contactLRU.Remove(element)
|
||||
}
|
||||
delete(c.personalPhotos, id)
|
||||
if element := c.personalElements[id]; element != nil {
|
||||
delete(c.personalElements, id)
|
||||
c.personalPhotoLRU.Remove(element)
|
||||
}
|
||||
for contactUserID := range c.reverseByOwner[id] {
|
||||
c.removeReverseKeyLocked(reverseContactKey{ownerUserID: id, contactUserID: contactUserID})
|
||||
}
|
||||
for contactUserID := range c.projectionByViewer[id] {
|
||||
c.removeContactProjectionKeyLocked(contactProjectionKey{viewerUserID: id, contactUserID: contactUserID})
|
||||
}
|
||||
for viewerUserID := range c.projectionByTarget[id] {
|
||||
c.removeContactProjectionKeyLocked(contactProjectionKey{viewerUserID: viewerUserID, contactUserID: id})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) removeReverseKeyLocked(key reverseContactKey) {
|
||||
if element, ok := c.reverse[key]; ok {
|
||||
c.removeReverseElementLocked(element)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) removeContactProjectionKeyLocked(key contactProjectionKey) {
|
||||
if element, ok := c.projection[key]; ok {
|
||||
c.removeContactProjectionElementLocked(element)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) FlushReadModelCache() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.epoch++
|
||||
c.flushGeneration++
|
||||
c.viewerGenerations = make(map[int64]uint64, 1024)
|
||||
c.contacts = make(map[int64]contactAccountSnapshot, 1024)
|
||||
c.contactElements = make(map[int64]*list.Element, 1024)
|
||||
c.contactLRU.Init()
|
||||
c.personalPhotos = make(map[int64]personalPhotoSnapshot, 1024)
|
||||
c.personalElements = make(map[int64]*list.Element, 1024)
|
||||
c.personalPhotoLRU.Init()
|
||||
c.reverse = make(map[reverseContactKey]*list.Element, 4096)
|
||||
c.reverseLRU.Init()
|
||||
c.reverseByOwner = make(map[int64]map[int64]struct{}, 1024)
|
||||
c.projection = make(map[contactProjectionKey]*list.Element, 4096)
|
||||
c.projectionLRU.Init()
|
||||
c.projectionByViewer = make(map[int64]map[int64]struct{}, 1024)
|
||||
c.projectionByTarget = make(map[int64]map[int64]struct{}, 1024)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) cacheEpoch() uint64 {
|
||||
func (c *CachedContactStore) captureCacheFence(userIDs ...int64) contactCacheFence {
|
||||
return c.captureCacheFenceSlices(userIDs, nil)
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) captureCacheFenceSlices(first, second []int64) contactCacheFence {
|
||||
c.mu.RLock()
|
||||
epoch := c.epoch
|
||||
fence := contactCacheFence{
|
||||
flushGeneration: c.flushGeneration,
|
||||
viewers: make([]contactCacheViewerFence, 0, len(first)+len(second)),
|
||||
}
|
||||
for _, userIDs := range [][]int64{first, second} {
|
||||
for _, userID := range userIDs {
|
||||
if userID == 0 {
|
||||
continue
|
||||
}
|
||||
fence.viewers = append(fence.viewers, contactCacheViewerFence{
|
||||
userID: userID,
|
||||
generation: c.viewerGenerations[userID],
|
||||
})
|
||||
}
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
return epoch
|
||||
return fence
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) cacheFenceCurrent(fence contactCacheFence) bool {
|
||||
c.mu.RLock()
|
||||
current := c.cacheFenceCurrentLocked(fence)
|
||||
c.mu.RUnlock()
|
||||
return current
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) cacheFenceCurrentLocked(fence contactCacheFence) bool {
|
||||
if c.flushGeneration != fence.flushGeneration {
|
||||
return false
|
||||
}
|
||||
for _, viewer := range fence.viewers {
|
||||
if c.viewerGenerations[viewer.userID] != viewer.generation {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *CachedContactStore) advanceViewerGenerationLocked(userID int64) {
|
||||
c.viewerGenerations[userID]++
|
||||
}
|
||||
|
||||
func buildContactAccountSnapshot(list domain.ContactList, expireAt time.Time) contactAccountSnapshot {
|
||||
|
|
@ -581,6 +1093,48 @@ func cloneCachedContactMap(in map[int64]domain.Contact) map[int64]domain.Contact
|
|||
return out
|
||||
}
|
||||
|
||||
func cloneContactProjectionBatch(in domain.ContactProjectionBatch) domain.ContactProjectionBatch {
|
||||
out := domain.ContactProjectionBatch{
|
||||
Contacts: make(map[int64]map[int64]domain.Contact, len(in.Contacts)),
|
||||
PersonalPhotos: make(map[int64]map[int64]domain.ProfilePhotoRef, len(in.PersonalPhotos)),
|
||||
}
|
||||
mergeContactProjectionBatch(&out, in)
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeContactProjectionBatch(dst *domain.ContactProjectionBatch, src domain.ContactProjectionBatch) {
|
||||
for viewerID, contacts := range src.Contacts {
|
||||
for targetID, contact := range contacts {
|
||||
putContactProjectionContact(dst, viewerID, targetID, contact)
|
||||
}
|
||||
}
|
||||
for viewerID, refs := range src.PersonalPhotos {
|
||||
for targetID, ref := range refs {
|
||||
putContactProjectionPersonalPhoto(dst, viewerID, targetID, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func putContactProjectionContact(batch *domain.ContactProjectionBatch, viewerID, targetID int64, contact domain.Contact) {
|
||||
if batch.Contacts == nil {
|
||||
batch.Contacts = map[int64]map[int64]domain.Contact{}
|
||||
}
|
||||
if batch.Contacts[viewerID] == nil {
|
||||
batch.Contacts[viewerID] = map[int64]domain.Contact{}
|
||||
}
|
||||
batch.Contacts[viewerID][targetID] = cloneCachedContact(contact)
|
||||
}
|
||||
|
||||
func putContactProjectionPersonalPhoto(batch *domain.ContactProjectionBatch, viewerID, targetID int64, ref domain.ProfilePhotoRef) {
|
||||
if batch.PersonalPhotos == nil {
|
||||
batch.PersonalPhotos = map[int64]map[int64]domain.ProfilePhotoRef{}
|
||||
}
|
||||
if batch.PersonalPhotos[viewerID] == nil {
|
||||
batch.PersonalPhotos[viewerID] = map[int64]domain.ProfilePhotoRef{}
|
||||
}
|
||||
batch.PersonalPhotos[viewerID][targetID] = cloneCachedProfilePhotoRef(ref)
|
||||
}
|
||||
|
||||
func dedupContactIDs(ids []int64) []int64 {
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
out := make([]int64, 0, len(ids))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue