228 lines
7.2 KiB
Go
228 lines
7.2 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/readmodelcache"
|
|
"telesrv/internal/store"
|
|
)
|
|
|
|
const (
|
|
peerIdentityReadModel = "peer_identity"
|
|
defaultPeerIdentityCacheMaxEntries = 1_000_000
|
|
)
|
|
|
|
type peerIdentityEntry struct {
|
|
hash int64
|
|
usernames []domain.Username
|
|
usernamesLoaded bool
|
|
verification domain.CustomVerification
|
|
verificationFound bool
|
|
verificationLoaded bool
|
|
}
|
|
|
|
// peerIdentityCache keeps both viewer-independent decorations under their one
|
|
// durable peer_identity token. A miss fills every configured facet even for a
|
|
// narrow update builder, so concurrent callers cannot overwrite one another
|
|
// with partial entries. The embedded hash is checked on every lookup; exact invalidation
|
|
// therefore never needs a process-wide load epoch that rejects unrelated peers.
|
|
type peerIdentityCache struct {
|
|
cache *readmodelcache.Cache[domain.Peer, peerIdentityEntry]
|
|
}
|
|
|
|
func newPeerIdentityCache(max int) *peerIdentityCache {
|
|
if max <= 0 {
|
|
max = defaultPeerIdentityCacheMaxEntries
|
|
}
|
|
return &peerIdentityCache{cache: readmodelcache.New[domain.Peer, peerIdentityEntry](
|
|
readmodelcache.Config[domain.Peer, peerIdentityEntry]{
|
|
MaxEntries: max,
|
|
Clone: func(in peerIdentityEntry) peerIdentityEntry {
|
|
in.usernames = append([]domain.Username(nil), in.usernames...)
|
|
return in
|
|
},
|
|
},
|
|
)}
|
|
}
|
|
|
|
func (c *peerIdentityCache) lookup(peer domain.Peer, hash int64) (peerIdentityEntry, bool) {
|
|
if c == nil || c.cache == nil || hash == 0 {
|
|
return peerIdentityEntry{}, false
|
|
}
|
|
entry, ok := c.cache.Peek(peer)
|
|
if !ok || entry.hash != hash {
|
|
return peerIdentityEntry{}, false
|
|
}
|
|
return entry, true
|
|
}
|
|
|
|
func (c *peerIdentityCache) store(peer domain.Peer, entry peerIdentityEntry) {
|
|
if c == nil || c.cache == nil || peer.ID == 0 || entry.hash == 0 {
|
|
return
|
|
}
|
|
c.cache.Store(peer, entry)
|
|
}
|
|
|
|
func (c *peerIdentityCache) invalidate(peer domain.Peer) {
|
|
if c != nil && c.cache != nil {
|
|
c.cache.Invalidate(peer)
|
|
}
|
|
}
|
|
|
|
func (c *peerIdentityCache) flush() {
|
|
if c != nil && c.cache != nil {
|
|
c.cache.Flush()
|
|
}
|
|
}
|
|
|
|
func (r *Router) peerIdentityHashes(ctx context.Context, peers []domain.Peer) (map[domain.Peer]int64, error) {
|
|
out := make(map[domain.Peer]int64, len(peers))
|
|
if r == nil || r.deps.ReadModelVersions == nil || len(peers) == 0 {
|
|
return out, nil
|
|
}
|
|
keys := make([]store.ReadModelKey, 0, len(peers))
|
|
seen := make(map[domain.Peer]struct{}, len(peers))
|
|
for _, peer := range peers {
|
|
if peer.ID == 0 || (peer.Type != domain.PeerTypeUser && peer.Type != domain.PeerTypeChannel) {
|
|
continue
|
|
}
|
|
if _, ok := seen[peer]; ok {
|
|
continue
|
|
}
|
|
seen[peer] = struct{}{}
|
|
keys = append(keys, store.ReadModelKey{
|
|
Model: peerIdentityReadModel,
|
|
PeerType: peer.Type,
|
|
PeerID: peer.ID,
|
|
})
|
|
}
|
|
rows, err := r.deps.ReadModelVersions.ReadModelHashes(ctx, keys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, key := range keys {
|
|
out[domain.Peer{Type: key.PeerType, ID: key.PeerID}] = rows[key]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// peerIdentityMaps resolves the requested facets in at most two batched backend
|
|
// reads, preserves positive and negative results, and revalidates the durable
|
|
// hash before admitting them. Username and verification failures are isolated:
|
|
// one optional backend cannot suppress the other decoration.
|
|
func (r *Router) peerIdentityMaps(
|
|
ctx context.Context,
|
|
peers []domain.Peer,
|
|
wantUsernames bool,
|
|
wantVerification bool,
|
|
) (map[domain.Peer][]domain.Username, map[domain.Peer]domain.CustomVerification) {
|
|
usernames := make(map[domain.Peer][]domain.Username)
|
|
verifications := make(map[domain.Peer]domain.CustomVerification)
|
|
if len(peers) == 0 || (!wantUsernames && !wantVerification) {
|
|
return usernames, verifications
|
|
}
|
|
projectUsernames := wantUsernames && r.deps.Usernames != nil
|
|
projectVerification := wantVerification && r.deps.BotVerifications != nil
|
|
if !projectUsernames && !projectVerification {
|
|
return usernames, verifications
|
|
}
|
|
// A cache miss fills every configured facet, even when a narrow caller only
|
|
// asks for one. This keeps a single immutable value per peer and prevents two
|
|
// concurrent narrow builders from overwriting each other's partial entry.
|
|
loadUsernames := r.deps.Usernames != nil
|
|
loadVerification := r.deps.BotVerifications != nil
|
|
|
|
// Without the durable token, load fresh data for this response but never
|
|
// reuse it. A token read failure is fail-closed for the cache as well.
|
|
if r.peerIdentityCache == nil || r.deps.ReadModelVersions == nil {
|
|
if projectUsernames {
|
|
if loaded, err := r.loadUsernameRegistryMap(ctx, peers); err == nil {
|
|
usernames = loaded
|
|
}
|
|
}
|
|
if projectVerification {
|
|
if loaded, err := r.loadBotVerificationMap(ctx, peers); err == nil {
|
|
verifications = loaded
|
|
}
|
|
}
|
|
return usernames, verifications
|
|
}
|
|
|
|
hashes, err := r.peerIdentityHashes(ctx, peers)
|
|
if err != nil {
|
|
return usernames, verifications
|
|
}
|
|
entries := make(map[domain.Peer]peerIdentityEntry, len(peers))
|
|
usernameMisses := make([]domain.Peer, 0, len(peers))
|
|
verificationMisses := make([]domain.Peer, 0, len(peers))
|
|
seen := make(map[domain.Peer]struct{}, len(peers))
|
|
for _, peer := range peers {
|
|
if _, ok := seen[peer]; ok || peer.ID == 0 {
|
|
continue
|
|
}
|
|
seen[peer] = struct{}{}
|
|
entry, _ := r.peerIdentityCache.lookup(peer, hashes[peer])
|
|
entry.hash = hashes[peer]
|
|
entries[peer] = entry
|
|
if loadUsernames && !entry.usernamesLoaded {
|
|
usernameMisses = append(usernameMisses, peer)
|
|
}
|
|
if loadVerification && !entry.verificationLoaded {
|
|
verificationMisses = append(verificationMisses, peer)
|
|
}
|
|
}
|
|
|
|
if len(usernameMisses) > 0 {
|
|
if loaded, loadErr := r.loadUsernameRegistryMap(ctx, usernameMisses); loadErr == nil {
|
|
for _, peer := range usernameMisses {
|
|
entry := entries[peer]
|
|
entry.usernames = append([]domain.Username(nil), loaded[peer]...)
|
|
entry.usernamesLoaded = true
|
|
entries[peer] = entry
|
|
}
|
|
}
|
|
}
|
|
if len(verificationMisses) > 0 {
|
|
if loaded, loadErr := r.loadBotVerificationMap(ctx, verificationMisses); loadErr == nil {
|
|
for _, peer := range verificationMisses {
|
|
entry := entries[peer]
|
|
entry.verification, entry.verificationFound = loaded[peer]
|
|
entry.verificationLoaded = true
|
|
entries[peer] = entry
|
|
}
|
|
}
|
|
}
|
|
|
|
// The version store is itself an exact-key L1 cache, so this revalidation
|
|
// adds no warm PostgreSQL trip. If a concurrent peer_identity mutation was
|
|
// observed, do not admit or project the pre-mutation value in this response.
|
|
currentHashes, err := r.peerIdentityHashes(ctx, peers)
|
|
if err != nil {
|
|
return usernames, verifications
|
|
}
|
|
for peer, entry := range entries {
|
|
if entry.hash == 0 || currentHashes[peer] != entry.hash {
|
|
continue
|
|
}
|
|
complete := (!loadUsernames || entry.usernamesLoaded) && (!loadVerification || entry.verificationLoaded)
|
|
if complete {
|
|
r.peerIdentityCache.store(peer, entry)
|
|
}
|
|
if projectUsernames && entry.usernamesLoaded && len(entry.usernames) > 0 {
|
|
usernames[peer] = append([]domain.Username(nil), entry.usernames...)
|
|
}
|
|
if projectVerification && entry.verificationLoaded && entry.verificationFound {
|
|
verifications[peer] = entry.verification
|
|
}
|
|
}
|
|
return usernames, verifications
|
|
}
|
|
|
|
func (r *Router) InvalidatePeerIdentityReadModel(peer domain.Peer) {
|
|
r.peerIdentityCache.invalidate(peer)
|
|
}
|
|
|
|
func (r *Router) FlushPeerIdentityReadModel() {
|
|
r.peerIdentityCache.flush()
|
|
}
|