fix: resolve im backend issue list regressions
This commit is contained in:
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user