mirror of
https://github.com/matrix-org/dendrite.git
synced 2026-01-01 03:03:10 -06:00
Partially implement lazy loading on /sync
This commit is contained in:
parent
afee6a76c0
commit
032aeb8b98
49
internal/caching/cache_lazy_load_members.go
Normal file
49
internal/caching/cache_lazy_load_members.go
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package caching
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LazyLoadCacheName = "lazy_load_members"
|
||||||
|
LazyLoadCacheMaxEntries = 128
|
||||||
|
LazyLoadCacheMutable = true
|
||||||
|
LazyLoadCacheMaxAge = time.Minute * 30
|
||||||
|
)
|
||||||
|
|
||||||
|
type LazyLoadCache struct {
|
||||||
|
*InMemoryLRUCachePartition
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLazyLoadCache creates a new InMemoryLRUCachePartition.
|
||||||
|
func NewLazyLoadCache() (*LazyLoadCache, error) {
|
||||||
|
cache, err := NewInMemoryLRUCachePartition(
|
||||||
|
LazyLoadCacheName,
|
||||||
|
LazyLoadCacheMutable,
|
||||||
|
LazyLoadCacheMaxEntries,
|
||||||
|
LazyLoadCacheMaxAge,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
go cacheCleaner(cache)
|
||||||
|
return &LazyLoadCache{cache}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LazyLoadCache) StoreLazyLoadedMembers(reqUser, deviceID, roomID, userID string, event *gomatrixserverlib.HeaderedEvent) {
|
||||||
|
cacheKey := fmt.Sprintf("%s/%s/%s/%s", reqUser, deviceID, roomID, userID)
|
||||||
|
c.Set(cacheKey, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LazyLoadCache) GetLazyLoadedMembers(reqUser, deviceID, roomID, userID string) (*gomatrixserverlib.HeaderedEvent, bool) {
|
||||||
|
cacheKey := fmt.Sprintf("%s/%s/%s/%s", reqUser, deviceID, roomID, userID)
|
||||||
|
val, ok := c.Get(cacheKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, ok
|
||||||
|
}
|
||||||
|
return val.(*gomatrixserverlib.HeaderedEvent), ok
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
"github.com/matrix-org/dendrite/syncapi/types"
|
"github.com/matrix-org/dendrite/syncapi/types"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
@ -26,7 +27,8 @@ type PDUStreamProvider struct {
|
||||||
|
|
||||||
tasks chan func()
|
tasks chan func()
|
||||||
workers atomic.Int32
|
workers atomic.Int32
|
||||||
userAPI userapi.UserInternalAPI
|
// userID+deviceID -> lazy loading cache
|
||||||
|
lazyLoadCache map[string]*caching.LazyLoadCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PDUStreamProvider) worker() {
|
func (p *PDUStreamProvider) worker() {
|
||||||
|
|
@ -188,7 +190,7 @@ func (p *PDUStreamProvider) IncrementalSync(
|
||||||
newPos = from
|
newPos = from
|
||||||
for _, delta := range stateDeltas {
|
for _, delta := range stateDeltas {
|
||||||
var pos types.StreamPosition
|
var pos types.StreamPosition
|
||||||
if pos, err = p.addRoomDeltaToResponse(ctx, req.Device, r, delta, &eventFilter, req.Response); err != nil {
|
if pos, err = p.addRoomDeltaToResponse(ctx, req.Device, r, delta, &eventFilter, &stateFilter, req.Response); err != nil {
|
||||||
req.Log.WithError(err).Error("d.addRoomDeltaToResponse failed")
|
req.Log.WithError(err).Error("d.addRoomDeltaToResponse failed")
|
||||||
return to
|
return to
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +211,7 @@ func (p *PDUStreamProvider) addRoomDeltaToResponse(
|
||||||
r types.Range,
|
r types.Range,
|
||||||
delta types.StateDelta,
|
delta types.StateDelta,
|
||||||
eventFilter *gomatrixserverlib.RoomEventFilter,
|
eventFilter *gomatrixserverlib.RoomEventFilter,
|
||||||
|
stateFilter *gomatrixserverlib.StateFilter,
|
||||||
res *types.Response,
|
res *types.Response,
|
||||||
) (types.StreamPosition, error) {
|
) (types.StreamPosition, error) {
|
||||||
if delta.MembershipPos > 0 && delta.Membership == gomatrixserverlib.Leave {
|
if delta.MembershipPos > 0 && delta.Membership == gomatrixserverlib.Leave {
|
||||||
|
|
@ -263,6 +266,17 @@ func (p *PDUStreamProvider) addRoomDeltaToResponse(
|
||||||
updateLatestPosition(delta.StateEvents[len(delta.StateEvents)-1].EventID())
|
updateLatestPosition(delta.StateEvents[len(delta.StateEvents)-1].EventID())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if stateFilter.LazyLoadMembers {
|
||||||
|
cache, err := p.getLazyLoadCache(device)
|
||||||
|
if err != nil {
|
||||||
|
return r.From, err
|
||||||
|
}
|
||||||
|
delta.StateEvents, err = p.lazyLoadMembers(ctx, delta.RoomID, true, stateFilter, device, recentEvents, cache, delta.StateEvents)
|
||||||
|
if err != nil {
|
||||||
|
return r.From, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hasMembershipChange := false
|
hasMembershipChange := false
|
||||||
for _, recentEvent := range recentStreamEvents {
|
for _, recentEvent := range recentStreamEvents {
|
||||||
if recentEvent.Type() == gomatrixserverlib.MRoomMember && recentEvent.StateKey() != nil {
|
if recentEvent.Type() == gomatrixserverlib.MRoomMember && recentEvent.StateKey() != nil {
|
||||||
|
|
@ -402,6 +416,18 @@ func (p *PDUStreamProvider) getJoinResponseForCompleteSync(
|
||||||
// "Can sync a room with a message with a transaction id" - which does a complete sync to check.
|
// "Can sync a room with a message with a transaction id" - which does a complete sync to check.
|
||||||
recentEvents := p.DB.StreamEventsToEvents(device, recentStreamEvents)
|
recentEvents := p.DB.StreamEventsToEvents(device, recentStreamEvents)
|
||||||
stateEvents = removeDuplicates(stateEvents, recentEvents)
|
stateEvents = removeDuplicates(stateEvents, recentEvents)
|
||||||
|
|
||||||
|
if stateFilter.LazyLoadMembers {
|
||||||
|
cache, err := p.getLazyLoadCache(device)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stateEvents, err = p.lazyLoadMembers(ctx, roomID, false, stateFilter, device, recentEvents, cache, stateEvents)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jr = types.NewJoinResponse()
|
jr = types.NewJoinResponse()
|
||||||
jr.Summary.JoinedMemberCount = &joinedCount
|
jr.Summary.JoinedMemberCount = &joinedCount
|
||||||
jr.Summary.InvitedMemberCount = &invitedCount
|
jr.Summary.InvitedMemberCount = &invitedCount
|
||||||
|
|
@ -412,6 +438,83 @@ func (p *PDUStreamProvider) getJoinResponseForCompleteSync(
|
||||||
return jr, nil
|
return jr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PDUStreamProvider) lazyLoadMembers(
|
||||||
|
ctx context.Context, roomID string, isIncremental bool,
|
||||||
|
stateFilter *gomatrixserverlib.StateFilter, device *userapi.Device,
|
||||||
|
timelineEvents []*gomatrixserverlib.HeaderedEvent, cache *caching.LazyLoadCache,
|
||||||
|
stateEvents []*gomatrixserverlib.HeaderedEvent,
|
||||||
|
) ([]*gomatrixserverlib.HeaderedEvent, error) {
|
||||||
|
if len(timelineEvents) == 0 {
|
||||||
|
return stateEvents, nil
|
||||||
|
}
|
||||||
|
// Work out if we need to include membership events
|
||||||
|
timelineUsers := make(map[string]struct{})
|
||||||
|
|
||||||
|
if !isIncremental {
|
||||||
|
timelineUsers[device.UserID] = struct{}{}
|
||||||
|
}
|
||||||
|
// add all users the client doesn't know about yet to a list
|
||||||
|
for _, event := range timelineEvents {
|
||||||
|
if event.Type() == gomatrixserverlib.MRoomMember && event.StateKey() != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// membership is not yet cached, add it to the list
|
||||||
|
if _, ok := cache.GetLazyLoadedMembers(device.UserID, device.ID, roomID, event.Sender()); !ok {
|
||||||
|
timelineUsers[event.Sender()] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove existing membership events we don't care about, e.g. users not in the timeline.events
|
||||||
|
newStateEvents := []*gomatrixserverlib.HeaderedEvent{}
|
||||||
|
for _, event := range stateEvents {
|
||||||
|
if event.Type() == gomatrixserverlib.MRoomMember && event.StateKey() != nil {
|
||||||
|
// We want this users membership event, keep it in the list
|
||||||
|
if _, ok := timelineUsers[event.Sender()]; ok {
|
||||||
|
newStateEvents = append(newStateEvents, event)
|
||||||
|
if !stateFilter.IncludeRedundantMembers {
|
||||||
|
cache.StoreLazyLoadedMembers(device.UserID, device.ID, roomID, event.Sender(), event)
|
||||||
|
}
|
||||||
|
delete(timelineUsers, event.Sender())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newStateEvents = append(newStateEvents, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wantUsers := make([]string, 0, len(timelineUsers))
|
||||||
|
for userID := range timelineUsers {
|
||||||
|
wantUsers = append(wantUsers, userID)
|
||||||
|
}
|
||||||
|
// Query missing membership events
|
||||||
|
memberships, err := p.DB.GetStateEventsForRoom(ctx, roomID, &gomatrixserverlib.StateFilter{
|
||||||
|
Limit: 100,
|
||||||
|
Senders: &wantUsers,
|
||||||
|
Types: &[]string{gomatrixserverlib.MRoomMember},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return stateEvents, err
|
||||||
|
}
|
||||||
|
// cache the membership events
|
||||||
|
for _, membership := range memberships {
|
||||||
|
cache.StoreLazyLoadedMembers(device.UserID, device.ID, roomID, membership.Sender(), membership)
|
||||||
|
}
|
||||||
|
stateEvents = append(newStateEvents, memberships...)
|
||||||
|
return stateEvents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PDUStreamProvider) getLazyLoadCache(device *userapi.Device) (*caching.LazyLoadCache, error) {
|
||||||
|
var err error
|
||||||
|
cacheKey := device.UserID + device.ID
|
||||||
|
cache, ok := p.lazyLoadCache[cacheKey]
|
||||||
|
if !ok {
|
||||||
|
cache, err = caching.NewLazyLoadCache()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.lazyLoadCache[cacheKey] = cache
|
||||||
|
}
|
||||||
|
return cache, nil
|
||||||
|
}
|
||||||
|
|
||||||
// addIgnoredUsersToFilter adds ignored users to the eventfilter and
|
// addIgnoredUsersToFilter adds ignored users to the eventfilter and
|
||||||
// the syncreq itself for further use in streams.
|
// the syncreq itself for further use in streams.
|
||||||
func (p *PDUStreamProvider) addIgnoredUsersToFilter(ctx context.Context, req *types.SyncRequest, eventFilter *gomatrixserverlib.RoomEventFilter) error {
|
func (p *PDUStreamProvider) addIgnoredUsersToFilter(ctx context.Context, req *types.SyncRequest, eventFilter *gomatrixserverlib.RoomEventFilter) error {
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,12 @@ type Streams struct {
|
||||||
func NewSyncStreamProviders(
|
func NewSyncStreamProviders(
|
||||||
d storage.Database, userAPI userapi.UserInternalAPI,
|
d storage.Database, userAPI userapi.UserInternalAPI,
|
||||||
rsAPI rsapi.RoomserverInternalAPI, keyAPI keyapi.KeyInternalAPI,
|
rsAPI rsapi.RoomserverInternalAPI, keyAPI keyapi.KeyInternalAPI,
|
||||||
eduCache *caching.EDUCache, notifier *notifier.Notifier,
|
eduCache *caching.EDUCache, lazyLoadCache map[string]*caching.LazyLoadCache, notifier *notifier.Notifier,
|
||||||
) *Streams {
|
) *Streams {
|
||||||
streams := &Streams{
|
streams := &Streams{
|
||||||
PDUStreamProvider: &PDUStreamProvider{
|
PDUStreamProvider: &PDUStreamProvider{
|
||||||
StreamProvider: StreamProvider{DB: d},
|
StreamProvider: StreamProvider{DB: d},
|
||||||
userAPI: userAPI,
|
lazyLoadCache: lazyLoadCache,
|
||||||
},
|
},
|
||||||
TypingStreamProvider: &TypingStreamProvider{
|
TypingStreamProvider: &TypingStreamProvider{
|
||||||
StreamProvider: StreamProvider{DB: d},
|
StreamProvider: StreamProvider{DB: d},
|
||||||
|
|
|
||||||
|
|
@ -337,71 +337,12 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if syncReq.Filter.Room.State.LazyLoadMembers {
|
|
||||||
for roomID, jr := range syncReq.Response.Rooms.Join {
|
|
||||||
jr.State.Events = rp.applyLazyLoadMembers(req.Context(), syncReq.Since.IsEmpty(), roomID, syncReq.Device.UserID, jr.Timeline.Events, jr.State.Events)
|
|
||||||
syncReq.Response.Rooms.Join[roomID] = jr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: syncReq.Response,
|
JSON: syncReq.Response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *RequestPool) applyLazyLoadMembers(
|
|
||||||
ctx context.Context, isInitial bool, roomID, userID string, timelineEvents, stateEvents []gomatrixserverlib.ClientEvent,
|
|
||||||
) []gomatrixserverlib.ClientEvent {
|
|
||||||
if len(stateEvents) == 0 || len(timelineEvents) == 0 {
|
|
||||||
return stateEvents
|
|
||||||
}
|
|
||||||
logrus.Debugf("before stateEvents: %d", len(stateEvents))
|
|
||||||
|
|
||||||
// First, get a list of users we need in the response
|
|
||||||
requiredUsers := make(map[string]bool)
|
|
||||||
if isInitial {
|
|
||||||
requiredUsers[userID] = true
|
|
||||||
}
|
|
||||||
for _, ev := range timelineEvents {
|
|
||||||
requiredUsers[ev.Sender] = true
|
|
||||||
}
|
|
||||||
// Filter out users who didn't send an event
|
|
||||||
newState := []gomatrixserverlib.ClientEvent{}
|
|
||||||
membershipEvents := []gomatrixserverlib.ClientEvent{}
|
|
||||||
for _, event := range stateEvents {
|
|
||||||
if event.Type != gomatrixserverlib.MRoomMember {
|
|
||||||
newState = append(newState, event)
|
|
||||||
} else {
|
|
||||||
// did the user send an event?
|
|
||||||
if requiredUsers[event.Sender] {
|
|
||||||
membershipEvents = append(membershipEvents, event)
|
|
||||||
delete(requiredUsers, event.Sender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Get all remaining users in the list
|
|
||||||
membershipToUser := make(map[string]*gomatrixserverlib.HeaderedEvent)
|
|
||||||
for userID := range requiredUsers {
|
|
||||||
membership, err := rp.db.GetStateEvent(ctx, roomID, gomatrixserverlib.MRoomMember, userID)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("failed to get membership event for user")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if membership != nil {
|
|
||||||
membershipToUser[userID] = membership
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Convert HeaderedEvent to ClientEvent
|
|
||||||
for _, evt := range membershipToUser {
|
|
||||||
newState = append(newState, gomatrixserverlib.HeaderedToClientEvent(evt, gomatrixserverlib.FormatSync))
|
|
||||||
}
|
|
||||||
|
|
||||||
result := append(newState, membershipEvents...)
|
|
||||||
logrus.Debugf("after stateEvents: %d", len(result))
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rp *RequestPool) OnIncomingKeyChangeRequest(req *http.Request, device *userapi.Device) util.JSONResponse {
|
func (rp *RequestPool) OnIncomingKeyChangeRequest(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
from := req.URL.Query().Get("from")
|
from := req.URL.Query().Get("from")
|
||||||
to := req.URL.Query().Get("to")
|
to := req.URL.Query().Get("to")
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,8 @@ func AddPublicRoutes(
|
||||||
|
|
||||||
eduCache := caching.NewTypingCache()
|
eduCache := caching.NewTypingCache()
|
||||||
notifier := notifier.NewNotifier()
|
notifier := notifier.NewNotifier()
|
||||||
streams := streams.NewSyncStreamProviders(syncDB, userAPI, rsAPI, keyAPI, eduCache, notifier)
|
lazyLoadCache := make(map[string]*caching.LazyLoadCache)
|
||||||
|
streams := streams.NewSyncStreamProviders(syncDB, userAPI, rsAPI, keyAPI, eduCache, lazyLoadCache, notifier)
|
||||||
notifier.SetCurrentPosition(streams.Latest(context.Background()))
|
notifier.SetCurrentPosition(streams.Latest(context.Background()))
|
||||||
if err = notifier.Load(context.Background(), syncDB); err != nil {
|
if err = notifier.Load(context.Background(), syncDB); err != nil {
|
||||||
logrus.WithError(err).Panicf("failed to load notifier ")
|
logrus.WithError(err).Panicf("failed to load notifier ")
|
||||||
|
|
|
||||||
|
|
@ -698,4 +698,9 @@ Ignore user in existing room
|
||||||
Ignore invite in full sync
|
Ignore invite in full sync
|
||||||
Ignore invite in incremental sync
|
Ignore invite in incremental sync
|
||||||
A filtered timeline reaches its limit
|
A filtered timeline reaches its limit
|
||||||
A change to displayname should not result in a full state sync
|
A change to displayname should not result in a full state sync
|
||||||
|
The only membership state included in an initial sync is for all the senders in the timeline
|
||||||
|
The only membership state included in an incremental sync is for senders in the timeline
|
||||||
|
Old members are included in gappy incr LL sync if they start speaking
|
||||||
|
We do send redundant membership state across incremental syncs if asked
|
||||||
|
Rejecting invite over federation doesn't break incremental /sync
|
||||||
Loading…
Reference in a new issue