From b76398f685dfcfe353be6fd56a0d2bc5645b2a76 Mon Sep 17 00:00:00 2001 From: kim Date: Tue, 8 Apr 2025 18:14:26 +0100 Subject: [PATCH] more code comments, don't bother inserting statuses if timeline not preloaded --- internal/cache/timeline/preload.go | 36 ++++++++++++++++---- internal/cache/timeline/status.go | 19 +++++++++-- internal/processing/timeline/timeline.go | 43 ++++++++++-------------- 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/internal/cache/timeline/preload.go b/internal/cache/timeline/preload.go index 71c8c7c69..7661e708f 100644 --- a/internal/cache/timeline/preload.go +++ b/internal/cache/timeline/preload.go @@ -33,10 +33,34 @@ import ( // - brand-new = nil (functionally same as 'needs preload') type preloader struct{ p atomic.Pointer[any] } -// Check will concurrency-safely check the preload -// state, and if needed call the provided function. -// if a preload is in progress, it will wait until complete. -func (p *preloader) Check(preload func()) { +// Check will return the current preload state, +// waiting if a preload is currently in progress. +func (p *preloader) Check() bool { + for { + // Get state ptr. + ptr := p.p.Load() + + // Check if requires preloading. + if ptr == nil || *ptr == false { + return false + } + + // Check for a preload currently in progress. + if wg, _ := (*ptr).(*sync.WaitGroup); wg != nil { + wg.Wait() + continue + } + + // Anything else + // means success. + return true + } +} + +// CheckPreload will safely check the preload state, +// and if needed call the provided function. if a +// preload is in progress, it will wait until complete. +func (p *preloader) CheckPreload(preload func()) { for { // Get state ptr. ptr := p.p.Load() @@ -95,7 +119,7 @@ func (p *preloader) start(old *any, preload func()) bool { // done marks state as preloaded, // i.e. no more preload required. -func (p *preloader) done() { +func (p *preloader) Done() { old := p.p.Swap(new(any)) if old == nil { // was brand-new return @@ -109,7 +133,7 @@ func (p *preloader) done() { // clear will clear the state, marking a "preload" as required. // i.e. next call to Check() will call provided preload func. -func (p *preloader) clear() { +func (p *preloader) Clear() { b := false a := any(b) for { diff --git a/internal/cache/timeline/status.go b/internal/cache/timeline/status.go index d76204599..214d1bcdd 100644 --- a/internal/cache/timeline/status.go +++ b/internal/cache/timeline/status.go @@ -32,6 +32,12 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) +// repeatBoostDepth determines the minimum count +// of statuses after which repeat boosts, or boosts +// of the original, may appear. This is may not end +// up *exact*, as small races between insert and the +// repeatBoost calculation may allow 1 or so extra +// to sneak in ahead of time. but it mostly works! const repeatBoostDepth = 40 // StatusMeta contains minimum viable metadata @@ -190,14 +196,14 @@ func (t *StatusTimeline) Preload( n int, err error, ) { - t.preloader.Check(func() { + t.preloader.CheckPreload(func() { n, err = t.preload(loadPage, filter) if err != nil { return } // Mark preloaded. - t.preloader.done() + t.preloader.Done() }) return } @@ -547,6 +553,13 @@ func loadStatusTimeline( // 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 timeline no preloaded, i.e. + // no-one using it, don't insert. + if !t.preloader.Check() { + return false + } + if status.BoostOfID != "" { // Check through top $repeatBoostDepth number of timeline items. for i, value := range t.cache.RangeUnsafe(structr.Desc) { @@ -718,7 +731,7 @@ func (t *StatusTimeline) Trim() { t.cache.Trim(t.cut, structr.Asc) } // Clear will mark the entire timeline as requiring preload, // which will trigger a clear and reload of the entire thing. -func (t *StatusTimeline) Clear() { t.preloader.clear() } +func (t *StatusTimeline) Clear() { t.preloader.Clear() } // prepareStatuses takes a slice of cached (or, freshly loaded!) StatusMeta{} // models, and use given function to return prepared frontend API models. diff --git a/internal/processing/timeline/timeline.go b/internal/processing/timeline/timeline.go index 1d22e2d5a..bb55230ab 100644 --- a/internal/processing/timeline/timeline.go +++ b/internal/processing/timeline/timeline.go @@ -108,29 +108,9 @@ func (p *Processor) getStatusTimeline( // input paging cursor. id.ValidatePage(page) - // Returned models and page params. - var apiStatuses []*apimodel.Status - var lo, hi string - - // Pre-prepared filter function that just ensures we - // don't end up serving multiple copies of the same boost. - prepare := func(status *gtsmodel.Status) (*apimodel.Status, error) { - apiStatus, err := p.converter.StatusToAPIStatus(ctx, - status, - requester, - filterCtx, - filters, - mutes, - ) - if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { - return nil, err - } - return apiStatus, nil - } - // Load status page via timeline cache, also // getting lo, hi values for next, prev pages. - apiStatuses, lo, hi, err = timeline.Load(ctx, + apiStatuses, lo, hi, err := timeline.Load(ctx, // Status page // to load. @@ -145,13 +125,24 @@ func (p *Processor) getStatusTimeline( return p.state.DB.GetStatusesByIDs(ctx, ids) }, - // Filtering function, - // i.e. filter before caching. + // Call provided status + // filtering function. filter, - // Frontend API model - // preparation function. - prepare, + // Frontend API model preparation function. + func(status *gtsmodel.Status) (*apimodel.Status, error) { + apiStatus, err := p.converter.StatusToAPIStatus(ctx, + status, + requester, + filterCtx, + filters, + mutes, + ) + if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { + return nil, err + } + return apiStatus, nil + }, ) if err != nil {