fix: harden single-machine load handling

This commit is contained in:
richjohnrambo
2026-09-29 17:52:13 +08:00
parent ab73b69870
commit 4b00b5ae37
24 changed files with 1030 additions and 144 deletions
@@ -3,6 +3,7 @@ package store
import (
"context"
"encoding/json"
"errors"
"strconv"
"strings"
"testing"
@@ -443,6 +444,34 @@ func TestInvalidateGroupSettingsUsesTopicCacheKey(t *testing.T) {
}
}
func TestGetGroupMuteUsesPositiveAndNegativeCache(t *testing.T) {
oldRuntime := groupMsgRuntime
runtime := newTestGroupMessageRuntime()
groupMsgRuntime = runtime
t.Cleanup(func() { groupMsgRuntime = oldRuntime })
tenantID := types.TenantID(7)
topic := "grpAlpha"
uid := types.Uid(42)
cacheID := groupMemberCacheID(topic, uid)
expires := time.Now().UTC().Add(time.Minute)
want := &types.GroupMute{TenantID: tenantID, Topic: topic, UserID: uid, Status: types.GroupMuteStatusActive, ExpiresAt: &expires}
if err := CacheObject(tenantID, cacheNamespaceGroupMute, cacheID, groupMuteCacheValue{Found: true, Mute: want}); err != nil {
t.Fatalf("cache positive mute: %v", err)
}
got, err := (groupBusinessMapper{}).GetMute(tenantID, topic, uid)
if err != nil || got == nil || got.UserID != uid || got.Status != types.GroupMuteStatusActive {
t.Fatalf("positive cache result: mute=%#v err=%v", got, err)
}
if err = CacheObject(tenantID, cacheNamespaceGroupMute, cacheID, groupMuteCacheValue{}); err != nil {
t.Fatalf("cache negative mute: %v", err)
}
if got, err = (groupBusinessMapper{}).GetMute(tenantID, topic, uid); !errors.Is(err, types.ErrNotFound) || got != nil {
t.Fatalf("negative cache result: mute=%#v err=%v", got, err)
}
}
func TestTopicAndSubscriptionCacheTTLIsShort(t *testing.T) {
runtime := &redisGroupMessageRuntime{cfg: redisConfig{CacheTTL: 300, ShortTTL: 600}}
for _, namespace := range []string{cacheNamespaceTopic, cacheNamespaceSub} {
+53 -3
View File
@@ -142,6 +142,7 @@ const (
cacheNamespaceSub = "sub"
cacheNamespaceGroupSettings = "group_settings"
cacheNamespaceGroupMember = "group_member:v2"
cacheNamespaceGroupMute = "group_mute:v1"
cacheNamespaceVerification = "verification"
cacheNamespaceActionToken = "action_token"
cacheNamespaceFriends = "friends"
@@ -2422,11 +2423,32 @@ func (groupBusinessMapper) GetMute(tenantID types.TenantID, topic string, uid ty
if strings.TrimSpace(topic) == "" || uid.IsZero() {
return nil, types.ErrMalformed
}
topic = strings.TrimSpace(topic)
cacheID := groupMemberCacheID(topic, uid)
var cached groupMuteCacheValue
if ok, err := GetCachedObject(tenantID, cacheNamespaceGroupMute, cacheID, &cached); err != nil {
logs.Warn.Printf("cache group mute[%s]: read failed: %v", cacheID, err)
_ = DeleteCachedObject(tenantID, cacheNamespaceGroupMute, cacheID)
} else if ok {
if !cached.Found || cached.Mute == nil {
return nil, types.ErrNotFound
}
if cached.Mute.ExpiresAt == nil || cached.Mute.ExpiresAt.After(time.Now().UTC()) {
return cached.Mute, nil
}
_ = DeleteCachedObject(tenantID, cacheNamespaceGroupMute, cacheID)
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantGroupMuteGet(tenantID, strings.TrimSpace(topic), uid)
mute, err := tenantAdp.TenantGroupMuteGet(tenantID, topic, uid)
if err == nil && mute != nil {
_ = CacheObject(tenantID, cacheNamespaceGroupMute, cacheID, groupMuteCacheValue{Found: true, Mute: mute})
} else if errors.Is(err, types.ErrNotFound) {
_ = CacheObject(tenantID, cacheNamespaceGroupMute, cacheID, groupMuteCacheValue{})
}
return mute, err
}
func (groupBusinessMapper) UpsertMute(tenantID types.TenantID, mute *types.GroupMute) (*types.GroupMute, error) {
@@ -2440,6 +2462,12 @@ func (groupBusinessMapper) UpsertMute(tenantID types.TenantID, mute *types.Group
updated, err := tenantAdp.TenantGroupMuteUpsert(tenantID, mute)
if err == nil {
invalidateGroupMemberPages(tenantID, mute.Topic)
cacheID := groupMemberCacheID(mute.Topic, mute.UserID)
if updated != nil {
_ = CacheObject(tenantID, cacheNamespaceGroupMute, cacheID, groupMuteCacheValue{Found: true, Mute: updated})
} else {
_ = DeleteCachedObject(tenantID, cacheNamespaceGroupMute, cacheID)
}
}
return updated, err
}
@@ -2456,6 +2484,7 @@ func (groupBusinessMapper) DeleteMute(tenantID types.TenantID, topic string, uid
return err
}
invalidateGroupMemberPages(tenantID, topic)
_ = CacheObject(tenantID, cacheNamespaceGroupMute, groupMemberCacheID(topic, uid), groupMuteCacheValue{})
return nil
}
@@ -2581,11 +2610,32 @@ func (groupBusinessMapper) CanPost(tenantID types.TenantID, topic string, uid ty
if strings.TrimSpace(topic) == "" || uid.IsZero() || now.IsZero() {
return false, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if admin {
return true, nil
}
topic = strings.TrimSpace(topic)
mapper := groupBusinessMapper{}
settings, err := mapper.GetSettings(tenantID, topic)
if err != nil && !errors.Is(err, types.ErrNotFound) {
return false, err
}
if settings != nil && settings.GroupStatus == types.GroupStatusActive && settings.MuteAll {
return false, nil
}
mute, err := mapper.GetMute(tenantID, topic, uid)
if errors.Is(err, types.ErrNotFound) {
return true, nil
}
if err != nil {
return false, err
}
return tenantAdp.TenantGroupCanPost(tenantID, strings.TrimSpace(topic), uid, admin, now)
return mute == nil || mute.Status != types.GroupMuteStatusActive ||
(mute.ExpiresAt != nil && !mute.ExpiresAt.After(now)), nil
}
type groupMuteCacheValue struct {
Found bool `json:"found"`
Mute *types.GroupMute `json:"mute,omitempty"`
}
func groupMemberCacheID(topic string, uid types.Uid) string {