fix: protect publish path from mysql exhaustion

This commit is contained in:
richjohnrambo
2026-09-27 18:35:54 +08:00
parent 36eb76a5e3
commit 3bb6774786
17 changed files with 308 additions and 21 deletions
+37 -4
View File
@@ -17,6 +17,7 @@ type testGroupMessageRuntime struct {
shortStates map[string]bool
shortValues map[string]string
objects map[string][]byte
sets map[string]map[string]bool
deletedPrefixes []string
deletedObjects []string
}
@@ -28,6 +29,7 @@ func newTestGroupMessageRuntime() *testGroupMessageRuntime {
shortStates: make(map[string]bool),
shortValues: make(map[string]string),
objects: make(map[string][]byte),
sets: make(map[string]map[string]bool),
}
}
@@ -186,16 +188,24 @@ func (r *testGroupMessageRuntime) DeleteCachedObjectsByPrefix(tenantID types.Ten
return nil
}
func (r *testGroupMessageRuntime) AddCachedSetMember(types.TenantID, string, string, string) error {
func (r *testGroupMessageRuntime) AddCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
key := strconv.FormatInt(int64(tenantID), 10) + ":" + namespace + ":" + id
if r.sets[key] == nil {
r.sets[key] = make(map[string]bool)
}
r.sets[key][member] = true
return nil
}
func (r *testGroupMessageRuntime) RemoveCachedSetMember(types.TenantID, string, string, string) error {
func (r *testGroupMessageRuntime) RemoveCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
key := strconv.FormatInt(int64(tenantID), 10) + ":" + namespace + ":" + id
delete(r.sets[key], member)
return nil
}
func (r *testGroupMessageRuntime) CachedSetContains(types.TenantID, string, string, string) (bool, error) {
return false, nil
func (r *testGroupMessageRuntime) CachedSetContains(tenantID types.TenantID, namespace, id, member string) (bool, error) {
key := strconv.FormatInt(int64(tenantID), 10) + ":" + namespace + ":" + id
return r.sets[key][member], nil
}
func (r *testGroupMessageRuntime) AllowRate(types.TenantID, string, int, time.Duration) (bool, int, time.Duration, error) {
@@ -355,6 +365,29 @@ func TestMessageRuntimeSeqAndClientMidIdempotency(t *testing.T) {
}
}
func TestCanSendP2PUsesWarmRelationshipCache(t *testing.T) {
oldRuntime := groupMsgRuntime
runtime := newTestGroupMessageRuntime()
groupMsgRuntime = runtime
t.Cleanup(func() { groupMsgRuntime = oldRuntime })
tenantID := types.TenantID(7)
userID := types.Uid(42)
peerID := types.Uid(43)
cacheFriendPair(tenantID, userID, peerID)
allowed, relationship, err := (relationsMapper{}).CanSendP2P(tenantID, userID, peerID)
if err != nil || !allowed || relationship != types.RelationshipFriend {
t.Fatalf("warm friend cache was not used: allowed=%v relationship=%q err=%v", allowed, relationship, err)
}
cacheBlockOneWay(tenantID, peerID, userID)
allowed, relationship, err = (relationsMapper{}).CanSendP2P(tenantID, userID, peerID)
if err != nil || allowed || relationship != types.RelationshipBlockedByPeer {
t.Fatalf("reverse block cache was not enforced: allowed=%v relationship=%q err=%v", allowed, relationship, err)
}
}
func TestBumpMessageSeqFloorNeverMovesBackward(t *testing.T) {
oldRuntime := groupMsgRuntime
runtime := newTestGroupMessageRuntime()
+26
View File
@@ -3147,6 +3147,29 @@ func (relationsMapper) GetRelationshipContext(tenantID types.TenantID, userID, p
}
func (relationsMapper) CanSendP2P(tenantID types.TenantID, userID, peerID types.Uid) (bool, string, error) {
if userID.IsZero() || peerID.IsZero() || userID == peerID {
return false, "", types.ErrMalformed
}
// A warm relationship cache is sufficient for the publish hot path. Block
// entries are checked first in both directions; friendship and block writes
// keep these sets coherent. Redis errors and cache misses fall back to the
// authoritative database path below.
blockCacheReady := true
if blocked, err := cachedBlockContains(tenantID, userID, peerID); err != nil {
blockCacheReady = false
} else if blocked {
return false, types.RelationshipBlockedByMe, nil
}
if blocked, err := cachedBlockContains(tenantID, peerID, userID); err != nil {
blockCacheReady = false
} else if blocked {
return false, types.RelationshipBlockedByPeer, nil
}
if blockCacheReady {
if friend, err := cachedFriendContains(tenantID, userID, peerID); err == nil && friend {
return true, types.RelationshipFriend, nil
}
}
ctx, err := (relationsMapper{}).GetRelationshipContext(tenantID, userID, peerID, "")
if err != nil {
return false, "", err
@@ -3168,6 +3191,9 @@ func (relationsMapper) BlockUser(tenantID types.TenantID, blockerID, blockedID t
block, err := tenantAdp.TenantUserBlockUpsert(tenantID, blockerID, blockedID, now)
if err == nil {
cacheBlockOneWay(tenantID, blockerID, blockedID)
// A positive friend cache must never outlive a new block. Removing both
// directions also makes a later block-cache eviction fall back to MySQL.
uncacheFriendPair(tenantID, blockerID, blockedID)
}
return block, err
}