merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
228
internal/rpc/peer_identity_cache.go
Normal file
228
internal/rpc/peer_identity_cache.go
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue