mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 06:52:25 -05:00
share a bunch of the database load code in timeline cache, don't clear timelines on relationship change
This commit is contained in:
parent
0982a4b671
commit
b04b4f8516
5 changed files with 148 additions and 172 deletions
217
internal/cache/timeline/status.go
vendored
217
internal/cache/timeline/status.go
vendored
|
|
@ -387,7 +387,8 @@ func (t *StatusTimeline) Preload(
|
|||
// but still easily manageable memory-wise.
|
||||
recentBoosts := make(map[string]int, t.cut)
|
||||
|
||||
// Iterate the entire timeline cache and mark repeat boosts.
|
||||
// Iterate timeline ascending (i.e. oldest -> newest), marking
|
||||
// entry IDs and marking down if boosts have been seen recently.
|
||||
for idx, value := range t.cache.RangeUnsafe(structr.Asc) {
|
||||
|
||||
// Store current ID in map.
|
||||
|
|
@ -426,9 +427,7 @@ func (t *StatusTimeline) Load(
|
|||
// to load status models of already cached entries in the timeline.
|
||||
loadIDs func(ids []string) (statuses []*gtsmodel.Status, err error),
|
||||
|
||||
// filter can be used to perform filtering of returned
|
||||
// statuses BEFORE insert into cache. i.e. this will effect
|
||||
// what actually gets stored in the timeline cache.
|
||||
// filter can be used to perform filtering of returned statuses.
|
||||
filter func(each *gtsmodel.Status) (delete bool, err error),
|
||||
|
||||
// prepareAPI should prepare internal status model to frontend API model.
|
||||
|
|
@ -439,13 +438,6 @@ func (t *StatusTimeline) Load(
|
|||
string, // hi
|
||||
error,
|
||||
) {
|
||||
switch {
|
||||
case page == nil:
|
||||
panic("nil page")
|
||||
case loadPage == nil:
|
||||
panic("nil load page func")
|
||||
}
|
||||
|
||||
// Get paging details.
|
||||
lo := page.Min.Value
|
||||
hi := page.Max.Value
|
||||
|
|
@ -470,12 +462,6 @@ func (t *StatusTimeline) Load(
|
|||
dir,
|
||||
)
|
||||
|
||||
// TODO: in time, we should think about (dynamically?) preloading
|
||||
// the timelines, and any page requests outside of the cached
|
||||
// range go straight to the database. otherwise there's peculiarities
|
||||
// that may arise due to concurrent new and old range inserts, also
|
||||
// requests for old page ranges are almost always going to be one-off.
|
||||
|
||||
// We now reset the lo,hi values to
|
||||
// represent the lowest and highest
|
||||
// index values of loaded statuses.
|
||||
|
|
@ -486,8 +472,7 @@ func (t *StatusTimeline) Load(
|
|||
// response values.
|
||||
lo, hi = "", ""
|
||||
|
||||
// Preallocate a slice of up-to-limit API models.
|
||||
apiStatuses := make([]*apimodel.Status, 0, limit)
|
||||
var apiStatuses []*apimodel.Status
|
||||
|
||||
if len(metas) > 0 {
|
||||
// Before we can do any filtering, we need
|
||||
|
|
@ -504,6 +489,9 @@ func (t *StatusTimeline) Load(
|
|||
// Update paging parameters used for next database query.
|
||||
nextPageParams(nextPg, metas[len(metas)-1].ID, order)
|
||||
|
||||
// Allocate slice of expected required API models.
|
||||
apiStatuses = make([]*apimodel.Status, 0, len(metas))
|
||||
|
||||
// Prepare frontend API models for
|
||||
// the cached statuses. For now this
|
||||
// also does its own extra filtering.
|
||||
|
|
@ -515,74 +503,23 @@ func (t *StatusTimeline) Load(
|
|||
)
|
||||
}
|
||||
|
||||
// Check if we need to call
|
||||
// through to the database.
|
||||
// If no cached timeline statuses
|
||||
// were found for page, we need to
|
||||
// call through to the database.
|
||||
if len(apiStatuses) == 0 {
|
||||
var err error
|
||||
|
||||
// Load a little more than
|
||||
// limit to reduce db calls.
|
||||
nextPg.Limit += 10
|
||||
|
||||
// Perform maximum of 5 load
|
||||
// attempts fetching statuses.
|
||||
for i := 0; i < 5; i++ {
|
||||
|
||||
// Load next timeline statuses.
|
||||
statuses, err := loadPage(nextPg)
|
||||
if err != nil {
|
||||
return nil, "", "", gtserror.Newf("error loading timeline: %w", err)
|
||||
}
|
||||
|
||||
// No more statuses from
|
||||
// load function = at end.
|
||||
if len(statuses) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if hi == "" {
|
||||
// Set hi returned paging
|
||||
// value if not already set.
|
||||
hi = statuses[0].ID
|
||||
}
|
||||
|
||||
// Update nextPg cursor parameter for next database query.
|
||||
nextPageParams(nextPg, statuses[len(statuses)-1].ID, order)
|
||||
|
||||
// Perform any filtering on newly loaded statuses.
|
||||
statuses, err = doStatusFilter(statuses, filter)
|
||||
if err != nil {
|
||||
return nil, "", "", gtserror.Newf("error filtering statuses: %w", err)
|
||||
}
|
||||
|
||||
// After filtering no more
|
||||
// statuses remain, retry.
|
||||
if len(statuses) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert to our cache type,
|
||||
// these will get inserted into
|
||||
// the cache in prepare() below.
|
||||
metas := toStatusMeta(nil, statuses)
|
||||
|
||||
// Prepare frontend API models for
|
||||
// the loaded statuses. For now this
|
||||
// also does its own extra filtering.
|
||||
apiStatuses = prepareStatuses(ctx,
|
||||
metas,
|
||||
prepareAPI,
|
||||
apiStatuses,
|
||||
limit,
|
||||
)
|
||||
|
||||
// If we have anything, return
|
||||
// here. Even if below limit.
|
||||
if len(apiStatuses) > 0 {
|
||||
|
||||
// Set returned lo status paging value.
|
||||
lo = apiStatuses[len(apiStatuses)-1].ID
|
||||
break
|
||||
}
|
||||
// Pass through to main timeline db load function.
|
||||
apiStatuses, lo, hi, err = loadStatusTimeline(ctx,
|
||||
nextPg,
|
||||
metas,
|
||||
apiStatuses,
|
||||
loadPage,
|
||||
filter,
|
||||
prepareAPI,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -608,7 +545,6 @@ func LoadStatusTimeline(
|
|||
ctx context.Context,
|
||||
page *paging.Page,
|
||||
loadPage func(page *paging.Page) (statuses []*gtsmodel.Status, err error),
|
||||
loadIDs func(ids []string) (statuses []*gtsmodel.Status, err error),
|
||||
filter func(each *gtsmodel.Status) (delete bool, err error),
|
||||
prepareAPI func(status *gtsmodel.Status) (apiStatus *apimodel.Status, err error),
|
||||
) (
|
||||
|
|
@ -617,38 +553,83 @@ func LoadStatusTimeline(
|
|||
string, // hi
|
||||
error,
|
||||
) {
|
||||
switch {
|
||||
case page == nil:
|
||||
panic("nil page")
|
||||
case loadPage == nil:
|
||||
panic("nil load page func")
|
||||
}
|
||||
|
||||
// Get paging details.
|
||||
lo := page.Min.Value
|
||||
hi := page.Max.Value
|
||||
limit := page.Limit
|
||||
order := page.Order()
|
||||
|
||||
// Use a copy of current page so
|
||||
// we can repeatedly update it.
|
||||
nextPg := new(paging.Page)
|
||||
*nextPg = *page
|
||||
nextPg.Min.Value = lo
|
||||
nextPg.Max.Value = hi
|
||||
|
||||
// We now reset the lo,hi values to
|
||||
// represent the lowest and highest
|
||||
// index values of loaded statuses.
|
||||
lo, hi = "", ""
|
||||
// Pass through to main timeline db load function.
|
||||
apiStatuses, lo, hi, err := loadStatusTimeline(ctx,
|
||||
nextPg,
|
||||
nil,
|
||||
nil,
|
||||
loadPage,
|
||||
filter,
|
||||
prepareAPI,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
// Preallocate a slice of up-to-limit API models.
|
||||
apiStatuses := make([]*apimodel.Status, 0, limit)
|
||||
if page.Order().Ascending() {
|
||||
// The caller always expects the statuses
|
||||
// to be returned in DESC order, but we
|
||||
// build the status slice in paging order.
|
||||
// If paging ASC, we need to reverse the
|
||||
// returned statuses and paging values.
|
||||
slices.Reverse(apiStatuses)
|
||||
lo, hi = hi, lo
|
||||
}
|
||||
|
||||
return apiStatuses, lo, hi, nil
|
||||
}
|
||||
|
||||
// loadStatusTimeline encapsulates most of the main
|
||||
// timeline-load-from-database logic, allowing both
|
||||
// the temporary LoadStatusTimeline() function AND
|
||||
// the main StatusTimeline{}.Load() function to share
|
||||
// as much logic as possible.
|
||||
//
|
||||
// TODO: it may be worth moving this into StatusTimeline{}.Load()
|
||||
// once the temporary function above has been removed. Or it may
|
||||
// still be worth keeping *some* database logic separate.
|
||||
func loadStatusTimeline(
|
||||
ctx context.Context,
|
||||
nextPg *paging.Page,
|
||||
metas []*StatusMeta,
|
||||
apiStatuses []*apimodel.Status,
|
||||
loadPage func(page *paging.Page) (statuses []*gtsmodel.Status, err error),
|
||||
filter func(each *gtsmodel.Status) (delete bool, err error),
|
||||
prepareAPI func(status *gtsmodel.Status) (apiStatus *apimodel.Status, err error),
|
||||
) (
|
||||
[]*apimodel.Status,
|
||||
string, // lo
|
||||
string, // hi
|
||||
error,
|
||||
) {
|
||||
if loadPage == nil {
|
||||
panic("nil load page func")
|
||||
}
|
||||
|
||||
// Lowest and highest ID
|
||||
// vals of loaded statuses.
|
||||
var lo, hi string
|
||||
|
||||
// Extract paging params.
|
||||
order := nextPg.Order()
|
||||
limit := nextPg.Limit
|
||||
|
||||
// Load a little more than
|
||||
// limit to reduce db calls.
|
||||
nextPg.Limit += 10
|
||||
|
||||
// Ensure we have a slice of meta objects to
|
||||
// use in later preparation of the API models.
|
||||
metas = xslices.GrowJust(metas[:0], nextPg.Limit)
|
||||
|
||||
// Ensure we have a slice of required frontend API models.
|
||||
apiStatuses = xslices.GrowJust(apiStatuses[:0], nextPg.Limit)
|
||||
|
||||
// Perform maximum of 5 load
|
||||
// attempts fetching statuses.
|
||||
for i := 0; i < 5; i++ {
|
||||
|
|
@ -689,7 +670,7 @@ func LoadStatusTimeline(
|
|||
// Convert to our cache type,
|
||||
// these will get inserted into
|
||||
// the cache in prepare() below.
|
||||
metas := toStatusMeta(nil, statuses)
|
||||
metas = toStatusMeta(metas[:0], statuses)
|
||||
|
||||
// Prepare frontend API models for
|
||||
// the loaded statuses. For now this
|
||||
|
|
@ -711,22 +692,12 @@ func LoadStatusTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
if order.Ascending() {
|
||||
// The caller always expects the statuses
|
||||
// to be returned in DESC order, but we
|
||||
// build the status slice in paging order.
|
||||
// If paging ASC, we need to reverse the
|
||||
// returned statuses and paging values.
|
||||
slices.Reverse(apiStatuses)
|
||||
lo, hi = hi, lo
|
||||
}
|
||||
|
||||
return apiStatuses, lo, hi, nil
|
||||
}
|
||||
|
||||
// InsertOne allows you to insert a single status into the timeline, with optional prepared API model,
|
||||
// the return value indicates whether the passed status has been boosted recently on the timeline.
|
||||
func (t *StatusTimeline) InsertOne(status *gtsmodel.Status, prepared *apimodel.Status) (repeatBoost bool) {
|
||||
// InsertOne allows you to insert a single status into the timeline, with optional prepared API model.
|
||||
// The return value indicates whether status should be skipped from streams, e.g. if already boosted recently.
|
||||
func (t *StatusTimeline) InsertOne(status *gtsmodel.Status, prepared *apimodel.Status) (skip bool) {
|
||||
if status.BoostOfID != "" {
|
||||
// Check through top $repeatBoostDepth number of timeline items.
|
||||
for i, value := range t.cache.RangeUnsafe(structr.Desc) {
|
||||
|
|
@ -737,7 +708,7 @@ func (t *StatusTimeline) InsertOne(status *gtsmodel.Status, prepared *apimodel.S
|
|||
// If inserted status has already been boosted, or original was posted
|
||||
// within last $repeatBoostDepth, we indicate it as a repeated boost.
|
||||
if value.ID == status.BoostOfID || value.BoostOfID == status.BoostOfID {
|
||||
repeatBoost = true
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -749,7 +720,7 @@ func (t *StatusTimeline) InsertOne(status *gtsmodel.Status, prepared *apimodel.S
|
|||
AccountID: status.AccountID,
|
||||
BoostOfID: status.BoostOfID,
|
||||
BoostOfAccountID: status.BoostOfAccountID,
|
||||
repeatBoost: repeatBoost,
|
||||
repeatBoost: skip,
|
||||
loaded: nil,
|
||||
prepared: prepared,
|
||||
}) > t.max {
|
||||
|
|
|
|||
|
|
@ -199,9 +199,6 @@ func (p *Processor) getStatusTimeline(
|
|||
apiStatuses, lo, hi, err = timelinepkg.LoadStatusTimeline(ctx,
|
||||
page,
|
||||
loadPage,
|
||||
func(ids []string) ([]*gtsmodel.Status, error) {
|
||||
return p.state.DB.GetStatusesByIDs(ctx, ids)
|
||||
},
|
||||
filter,
|
||||
prepare,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -683,13 +683,19 @@ func (p *clientAPI) CreateBlock(ctx context.Context, cMsg *messages.FromClientAP
|
|||
}
|
||||
|
||||
if block.Account.IsLocal() {
|
||||
// Perform timeline invalidation for block origin account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, block.AccountID)
|
||||
// Remove posts by target from origin's timelines.
|
||||
p.surface.removeRelationshipFromTimelines(ctx,
|
||||
block.AccountID,
|
||||
block.TargetAccountID,
|
||||
)
|
||||
}
|
||||
|
||||
if block.TargetAccount.IsLocal() {
|
||||
// Perform timeline invalidation for block target account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, block.TargetAccountID)
|
||||
// Remove posts by origin from target's timelines.
|
||||
p.surface.removeRelationshipFromTimelines(ctx,
|
||||
block.TargetAccountID,
|
||||
block.AccountID,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: same with notifications?
|
||||
|
|
@ -851,13 +857,19 @@ func (p *clientAPI) UndoFollow(ctx context.Context, cMsg *messages.FromClientAPI
|
|||
}
|
||||
|
||||
if follow.Account.IsLocal() {
|
||||
// Perform timeline invalidation for block origin account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, follow.AccountID)
|
||||
// Remove posts by target from origin's timelines.
|
||||
p.surface.removeRelationshipFromTimelines(ctx,
|
||||
follow.AccountID,
|
||||
follow.TargetAccountID,
|
||||
)
|
||||
}
|
||||
|
||||
if follow.TargetAccount.IsLocal() {
|
||||
// Perform timeline invalidation for block target account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, follow.TargetAccountID)
|
||||
// Remove posts by origin from target's timelines.
|
||||
p.surface.removeRelationshipFromTimelines(ctx,
|
||||
follow.TargetAccountID,
|
||||
follow.AccountID,
|
||||
)
|
||||
}
|
||||
|
||||
if err := p.federate.UndoFollow(ctx, follow); err != nil {
|
||||
|
|
@ -873,16 +885,6 @@ func (p *clientAPI) UndoBlock(ctx context.Context, cMsg *messages.FromClientAPI)
|
|||
return gtserror.Newf("%T not parseable as *gtsmodel.Block", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
if block.Account.IsLocal() {
|
||||
// Perform timeline invalidation for block origin account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, block.AccountID)
|
||||
}
|
||||
|
||||
if block.TargetAccount.IsLocal() {
|
||||
// Perform timeline invalidation for block target account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, block.TargetAccountID)
|
||||
}
|
||||
|
||||
if err := p.federate.UndoBlock(ctx, block); err != nil {
|
||||
log.Errorf(ctx, "error federating block undo: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -715,13 +715,19 @@ func (p *fediAPI) CreateBlock(ctx context.Context, fMsg *messages.FromFediAPI) e
|
|||
}
|
||||
|
||||
if block.Account.IsLocal() {
|
||||
// Perform timeline invalidation for block origin account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, block.AccountID)
|
||||
// Remove posts by target from origin's timelines.
|
||||
p.surface.removeRelationshipFromTimelines(ctx,
|
||||
block.AccountID,
|
||||
block.TargetAccountID,
|
||||
)
|
||||
}
|
||||
|
||||
if block.TargetAccount.IsLocal() {
|
||||
// Perform timeline invalidation for block target account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, block.TargetAccountID)
|
||||
// Remove posts by origin from target's timelines.
|
||||
p.surface.removeRelationshipFromTimelines(ctx,
|
||||
block.TargetAccountID,
|
||||
block.AccountID,
|
||||
)
|
||||
}
|
||||
|
||||
// Remove any follows that existed between blocker + blockee.
|
||||
|
|
@ -1202,33 +1208,31 @@ func (p *fediAPI) UndoFollow(ctx context.Context, fMsg *messages.FromFediAPI) er
|
|||
}
|
||||
|
||||
if follow.Account.IsLocal() {
|
||||
// Perform timeline invalidation for block origin account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, follow.AccountID)
|
||||
// Remove posts by target from origin's timelines.
|
||||
p.surface.removeRelationshipFromTimelines(ctx,
|
||||
follow.AccountID,
|
||||
follow.TargetAccountID,
|
||||
)
|
||||
}
|
||||
|
||||
if follow.TargetAccount.IsLocal() {
|
||||
// Perform timeline invalidation for block target account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, follow.TargetAccountID)
|
||||
// Remove posts by origin from target's timelines.
|
||||
p.surface.removeRelationshipFromTimelines(ctx,
|
||||
follow.TargetAccountID,
|
||||
follow.AccountID,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) UndoBlock(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
block, ok := fMsg.GTSModel.(*gtsmodel.Block)
|
||||
_, ok := fMsg.GTSModel.(*gtsmodel.Block)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Block", fMsg.GTSModel)
|
||||
}
|
||||
|
||||
if block.Account.IsLocal() {
|
||||
// Perform timeline invalidation for block origin account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, block.AccountID)
|
||||
}
|
||||
|
||||
if block.TargetAccount.IsLocal() {
|
||||
// Perform timeline invalidation for block target account.
|
||||
p.surface.invalidateTimelinesForAccount(ctx, block.TargetAccountID)
|
||||
}
|
||||
// TODO: any required changes
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -826,21 +826,23 @@ func (s *Surface) removeTimelineEntriesByAccount(accountID string) {
|
|||
s.State.Caches.Timelines.List.RemoveByAccountIDs(accountID)
|
||||
}
|
||||
|
||||
// invalidateTimelinesForAccount invalidates all timeline caches stored for given account ID.
|
||||
func (s *Surface) invalidateTimelinesForAccount(ctx context.Context, accountID string) {
|
||||
|
||||
// There's a lot of visibility changes to caclculate for any
|
||||
// relationship change, so just clear all account's timelines.
|
||||
s.State.Caches.Timelines.Home.Clear(accountID)
|
||||
func (s *Surface) removeRelationshipFromTimelines(ctx context.Context, timelineAccountID string, targetAccountID string) {
|
||||
// Remove all statuses by target account
|
||||
// from given account's home timeline.
|
||||
s.State.Caches.Timelines.Home.
|
||||
MustGet(timelineAccountID).
|
||||
RemoveByAccountIDs(targetAccountID)
|
||||
|
||||
// Get the IDs of all the lists owned by the given account ID.
|
||||
listIDs, err := s.State.DB.GetListIDsByAccountID(ctx, accountID)
|
||||
listIDs, err := s.State.DB.GetListIDsByAccountID(ctx, timelineAccountID)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting lists for account %s: %v", accountID, err)
|
||||
log.Errorf(ctx, "error getting lists for account %s: %v", timelineAccountID, err)
|
||||
}
|
||||
|
||||
// Clear list timelines of account.
|
||||
for _, listID := range listIDs {
|
||||
s.State.Caches.Timelines.List.Clear(listID)
|
||||
// Remove all statuses by target account
|
||||
// from given account's list timelines.
|
||||
s.State.Caches.Timelines.List.MustGet(listID).
|
||||
RemoveByAccountIDs(targetAccountID)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue