fix: resolve im backend issue list regressions

This commit is contained in:
richjohnrambo
2026-09-17 14:59:16 +08:00
parent 760de35e10
commit 50f8581667
13 changed files with 750 additions and 181 deletions
+66 -5
View File
@@ -2,6 +2,9 @@ package store
import (
"context"
"encoding/json"
"strconv"
"strings"
"testing"
"time"
@@ -12,6 +15,7 @@ type testGroupMessageRuntime struct {
claims map[string]*groupMessageUniqueRecord
seqs map[string]int64
shortStates map[string]bool
objects map[string][]byte
deletedPrefixes []string
deletedObjects []string
}
@@ -21,6 +25,7 @@ func newTestGroupMessageRuntime() *testGroupMessageRuntime {
claims: make(map[string]*groupMessageUniqueRecord),
seqs: make(map[string]int64),
shortStates: make(map[string]bool),
objects: make(map[string][]byte),
}
}
@@ -145,21 +150,37 @@ func (r *testGroupMessageRuntime) StartRealtimeFanoutSubscriber(context.Context,
return nil
}
func (r *testGroupMessageRuntime) CacheObject(types.TenantID, string, string, any) error {
func (r *testGroupMessageRuntime) CacheObject(tenantID types.TenantID, namespace, id string, value any) error {
payload, err := json.Marshal(value)
if err != nil {
return err
}
r.objects[strconv.FormatInt(int64(tenantID), 10)+":"+namespace+":"+id] = payload
return nil
}
func (r *testGroupMessageRuntime) GetCachedObject(types.TenantID, string, string, any) (bool, error) {
return false, nil
func (r *testGroupMessageRuntime) GetCachedObject(tenantID types.TenantID, namespace, id string, value any) (bool, error) {
payload, ok := r.objects[strconv.FormatInt(int64(tenantID), 10)+":"+namespace+":"+id]
if !ok {
return false, nil
}
return true, json.Unmarshal(payload, value)
}
func (r *testGroupMessageRuntime) DeleteCachedObject(_ types.TenantID, namespace, id string) error {
func (r *testGroupMessageRuntime) DeleteCachedObject(tenantID types.TenantID, namespace, id string) error {
r.deletedObjects = append(r.deletedObjects, namespace+":"+id)
delete(r.objects, strconv.FormatInt(int64(tenantID), 10)+":"+namespace+":"+id)
return nil
}
func (r *testGroupMessageRuntime) DeleteCachedObjectsByPrefix(_ types.TenantID, _ string, prefix string) error {
func (r *testGroupMessageRuntime) DeleteCachedObjectsByPrefix(tenantID types.TenantID, namespace, prefix string) error {
r.deletedPrefixes = append(r.deletedPrefixes, prefix)
cachePrefix := strconv.FormatInt(int64(tenantID), 10) + ":" + namespace + ":" + prefix
for key := range r.objects {
if strings.HasPrefix(key, cachePrefix) {
delete(r.objects, key)
}
}
return nil
}
@@ -220,6 +241,46 @@ func TestLoginTokenRevocation(t *testing.T) {
}
}
func TestGroupMemberPageCacheRoundTripAndInvalidate(t *testing.T) {
oldRuntime := groupMsgRuntime
runtime := newTestGroupMessageRuntime()
groupMsgRuntime = runtime
t.Cleanup(func() { groupMsgRuntime = oldRuntime })
opts := types.GroupMemberListOptions{
Role: "member",
Query: " alice ",
Cursor: "42",
Limit: 20,
Mentionable: true,
}
key := groupMemberPageCacheID("grpAlpha", opts)
if !strings.HasPrefix(key, "grpAlpha|") {
t.Fatalf("cache key must be topic-prefixed for invalidation: %q", key)
}
CacheGroupMemberPage(7, key, groupMemberPageCache{
Items: []types.GroupMember{{
ID: 43,
Topic: "grpAlpha",
UserID: types.Uid(100),
Role: types.GroupMemberRoleMember,
}},
NextCursor: "43",
})
cached, ok := GetCachedGroupMemberPage(7, key)
if !ok || cached.NextCursor != "43" || len(cached.Items) != 1 || cached.Items[0].UserID != types.Uid(100) {
t.Fatalf("unexpected cached page: ok=%v page=%#v", ok, cached)
}
invalidateGroupMemberPages(7, "grpAlpha")
if _, ok := GetCachedGroupMemberPage(7, key); ok {
t.Fatalf("group member page cache survived invalidation")
}
if len(runtime.deletedPrefixes) != 1 || runtime.deletedPrefixes[0] != "grpAlpha|" {
t.Fatalf("unexpected invalidation prefixes: %#v", runtime.deletedPrefixes)
}
}
func TestRealtimeFanoutDedupClaimIDIsNodeScoped(t *testing.T) {
tenantID := types.TenantID(7)
dedupKey := "data:p2pABC:42"
+56 -9
View File
@@ -1760,6 +1760,9 @@ func (groupBusinessMapper) GetSettings(tenantID types.TenantID, topic string) (*
var cached types.GroupSettings
if ok, err := GetCachedObject(tenantID, cacheNamespaceGroupSettings, topic, &cached); err != nil {
logs.Warn.Printf("cache group settings[%s]: read failed: %v", topic, err)
if delErr := DeleteCachedObject(tenantID, cacheNamespaceGroupSettings, topic); delErr != nil {
logs.Warn.Printf("cache group settings[%s]: delete corrupted cache failed: %v", topic, delErr)
}
} else if ok {
return &cached, nil
}
@@ -1874,15 +1877,51 @@ func (groupBusinessMapper) ListMembers(tenantID types.TenantID, topic string, op
if opts.Limit <= 0 || opts.Limit > 100 {
opts.Limit = 30
}
topic = strings.TrimSpace(topic)
opts.Role = strings.TrimSpace(opts.Role)
opts.EligibleRole = strings.TrimSpace(opts.EligibleRole)
opts.Query = strings.TrimSpace(opts.Query)
opts.Cursor = strings.TrimSpace(opts.Cursor)
cacheKey := groupMemberPageCacheID(topic, opts)
if cached, ok := GetCachedGroupMemberPage(tenantID, cacheKey); ok {
return cached.Items, cached.NextCursor, nil
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantGroupMemberList(tenantID, strings.TrimSpace(topic), opts)
members, nextCursor, err := tenantAdp.TenantGroupMemberList(tenantID, topic, opts)
if err != nil {
return nil, "", err
}
CacheGroupMemberPage(tenantID, cacheKey, groupMemberPageCache{
Items: members,
NextCursor: nextCursor,
})
return members, nextCursor, nil
}
func CacheGroupMemberPage(tenantID types.TenantID, key string, payload map[string]any) {
if strings.TrimSpace(key) == "" || payload == nil {
type groupMemberPageCache struct {
Items []types.GroupMember `json:"items"`
NextCursor string `json:"next_cursor"`
}
func groupMemberPageCacheID(topic string, opts types.GroupMemberListOptions) string {
limit := opts.Limit
if limit <= 0 || limit > 100 {
limit = 30
}
return strings.TrimSpace(topic) +
"|role=" + strconv.Quote(strings.TrimSpace(opts.Role)) +
"|eligible_role=" + strconv.Quote(strings.TrimSpace(opts.EligibleRole)) +
"|query=" + strconv.Quote(strings.TrimSpace(opts.Query)) +
"|cursor=" + strconv.Quote(strings.TrimSpace(opts.Cursor)) +
"|limit=" + strconv.Itoa(limit) +
"|mentionable=" + strconv.FormatBool(opts.Mentionable)
}
func CacheGroupMemberPage(tenantID types.TenantID, key string, payload groupMemberPageCache) {
if strings.TrimSpace(key) == "" {
return
}
if err := CacheObject(tenantID, cacheNamespaceGroupPage, strings.TrimSpace(key), payload); err != nil {
@@ -1890,17 +1929,17 @@ func CacheGroupMemberPage(tenantID types.TenantID, key string, payload map[strin
}
}
func GetCachedGroupMemberPage(tenantID types.TenantID, key string) (map[string]any, bool) {
func GetCachedGroupMemberPage(tenantID types.TenantID, key string) (groupMemberPageCache, bool) {
if strings.TrimSpace(key) == "" {
return nil, false
return groupMemberPageCache{}, false
}
var payload map[string]any
var payload groupMemberPageCache
if ok, err := GetCachedObject(tenantID, cacheNamespaceGroupPage, strings.TrimSpace(key), &payload); err != nil {
logs.Warn.Printf("cache group member page[%s]: read failed: %v", key, err)
} else if ok {
return payload, true
}
return nil, false
return groupMemberPageCache{}, false
}
func invalidateGroupMemberPages(tenantID types.TenantID, topic string) {
@@ -2113,7 +2152,11 @@ func (groupBusinessMapper) UpsertMute(tenantID types.TenantID, mute *types.Group
if err != nil {
return nil, err
}
return tenantAdp.TenantGroupMuteUpsert(tenantID, mute)
updated, err := tenantAdp.TenantGroupMuteUpsert(tenantID, mute)
if err == nil {
invalidateGroupMemberPages(tenantID, mute.Topic)
}
return updated, err
}
func (groupBusinessMapper) DeleteMute(tenantID types.TenantID, topic string, uid, actor types.Uid, now time.Time) error {
@@ -2124,7 +2167,11 @@ func (groupBusinessMapper) DeleteMute(tenantID types.TenantID, topic string, uid
if err != nil {
return err
}
return tenantAdp.TenantGroupMuteDelete(tenantID, strings.TrimSpace(topic), uid, actor, now)
if err := tenantAdp.TenantGroupMuteDelete(tenantID, strings.TrimSpace(topic), uid, actor, now); err != nil {
return err
}
invalidateGroupMemberPages(tenantID, topic)
return nil
}
func (groupBusinessMapper) ListFiles(tenantID types.TenantID, topic, fileType, query, cursor string, limit int) ([]types.GroupFile, string, error) {
+2
View File
@@ -171,6 +171,8 @@ type GroupAnnouncement struct {
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
AuthorUserIDRaw int64 `db:"author_user_id"`
ReadCount int `db:"read_count"`
MemberCount int `db:"member_count"`
}
type GroupMute struct {
+17 -15
View File
@@ -29,24 +29,26 @@ type UserProfilePatch struct {
}
type UserPrivacySettings struct {
UserID Uid
AddMeMethods map[string]bool
ProfileVisibility string
LastSeenVisibility string
ReadReceipts bool
GroupInviteVerification bool
Version int
CreatedAt time.Time
UpdatedAt time.Time
UserID Uid
AddMeMethods map[string]bool
ProfileVisibility string
LastSeenVisibility string
ReadReceipts bool
FriendRequestVerification bool
GroupInviteVerification bool
Version int
CreatedAt time.Time
UpdatedAt time.Time
}
type UserPrivacyPatch struct {
AddMeMethods map[string]bool
ProfileVisibility *string
LastSeenVisibility *string
ReadReceipts *bool
GroupInviteVerification *bool
Version int
AddMeMethods map[string]bool
ProfileVisibility *string
LastSeenVisibility *string
ReadReceipts *bool
FriendRequestVerification *bool
GroupInviteVerification *bool
Version int
}
type AppRelease struct {