remove local / public caches (is out of scope for this work), share more timeline code

This commit is contained in:
kim 2025-04-03 13:51:47 +01:00
commit 227d6edc3e
10 changed files with 256 additions and 180 deletions

View file

@ -57,8 +57,8 @@ func (p *Processor) publicTimelineGet(
// account.
requester,
// Keyed-by-account-ID, public timeline cache.
p.state.Caches.Timelines.Public.MustGet(requester.ID),
// No cache.
nil,
// Current
// page.
@ -106,8 +106,8 @@ func (p *Processor) localTimelineGet(
// account.
requester,
// Keyed-by-account-ID, local timeline cache.
p.state.Caches.Timelines.Local.MustGet(requester.ID),
// No cache.
nil,
// Current
// page.

View file

@ -154,9 +154,6 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
suite.FailNow("precondition failed: status we would filter isn't present in unfiltered timeline")
}
// Clear the timeline to drop all cached statuses.
suite.state.Caches.Timelines.Public.ClearAll()
// Create a filter to hide one status on the timeline.
if err := suite.db.PutFilter(ctx, filter); err != nil {
suite.FailNow(err.Error())

View file

@ -20,20 +20,15 @@ package timeline
import (
"context"
"errors"
"fmt"
"slices"
"net/http"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
"github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/paging"
"github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// TagTimelineGet gets a pageable timeline for the given
@ -42,49 +37,69 @@ import (
// to requestingAcct before returning it.
func (p *Processor) TagTimelineGet(
ctx context.Context,
requestingAcct *gtsmodel.Account,
requester *gtsmodel.Account,
tagName string,
maxID string,
sinceID string,
minID string,
limit int,
) (*apimodel.PageableResponse, gtserror.WithCode) {
// Fetch the requested tag with name.
tag, errWithCode := p.getTag(ctx, tagName)
if errWithCode != nil {
return nil, errWithCode
}
// Check for a useable returned tag for endpoint.
if tag == nil || !*tag.Useable || !*tag.Listable {
// Obey mastodon API by returning 404 for this.
err := fmt.Errorf("tag was not found, or not useable/listable on this instance")
return nil, gtserror.NewErrorNotFound(err, err.Error())
const text = "tag was not found, or not useable/listable on this instance"
return nil, gtserror.NewWithCode(http.StatusNotFound, text)
}
page := paging.Page{
Min: paging.EitherMinID(minID, sinceID),
Max: paging.MaxID(maxID),
Limit: limit,
}
// Fetch status timeline for tag.
return p.getStatusTimeline(ctx,
statuses, err := p.state.DB.GetTagTimeline(ctx, tag.ID, &page)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = gtserror.Newf("db error getting statuses: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Auth'd
// account.
requester,
if page.Order().Ascending() {
// Returned statuses always
// need to be in DESC order.
slices.Reverse(statuses)
}
// No cache.
nil,
return p.packageTagResponse(
ctx,
requestingAcct,
statuses,
limit,
// Use API URL for tag.
// Current
// page.
&paging.Page{
Min: paging.EitherMinID(minID, sinceID),
Max: paging.MaxID(maxID),
Limit: limit,
},
// Tag timeline name's endpoint.
"/api/v1/timelines/tag/"+tagName,
// No page
// query.
nil,
// Status filter context.
statusfilter.FilterContextPublic,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
return p.state.DB.GetTagTimeline(ctx, tag.ID, pg)
},
// Filtering function,
// i.e. filter before caching.
func(s *gtsmodel.Status) (bool, error) {
// Check the visibility of passed status to requesting user.
ok, err := p.visFilter.StatusHomeTimelineable(ctx, requester, s)
return !ok, err
},
)
}
@ -106,69 +121,3 @@ func (p *Processor) getTag(ctx context.Context, tagName string) (*gtsmodel.Tag,
return tag, nil
}
func (p *Processor) packageTagResponse(
ctx context.Context,
requestingAcct *gtsmodel.Account,
statuses []*gtsmodel.Status,
limit int,
requestPath string,
) (*apimodel.PageableResponse, gtserror.WithCode) {
count := len(statuses)
if count == 0 {
return util.EmptyPageableResponse(), nil
}
var (
items = make([]interface{}, 0, count)
// Set next + prev values before filtering and API
// converting, so caller can still page properly.
nextMaxIDValue = statuses[count-1].ID
prevMinIDValue = statuses[0].ID
)
filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAcct.ID)
if err != nil {
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAcct.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), requestingAcct.ID, nil)
if err != nil {
err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", requestingAcct.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
compiledMutes := usermute.NewCompiledUserMuteList(mutes)
for _, s := range statuses {
timelineable, err := p.visFilter.StatusTagTimelineable(ctx, requestingAcct, s)
if err != nil {
log.Errorf(ctx, "error checking status visibility: %v", err)
continue
}
if !timelineable {
continue
}
apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requestingAcct, statusfilter.FilterContextPublic, filters, compiledMutes)
if errors.Is(err, statusfilter.ErrHideStatus) {
continue
}
if err != nil {
log.Errorf(ctx, "error converting to api status: %v", err)
continue
}
items = append(items, apiStatus)
}
return util.PackagePageableResponse(util.PageableResponseParams{
Items: items,
Path: requestPath,
NextMaxIDValue: nextMaxIDValue,
PrevMinIDValue: prevMinIDValue,
Limit: limit,
})
}

View file

@ -64,7 +64,7 @@ func New(state *state.State, converter *typeutils.Converter, visFilter *visibili
func (p *Processor) getStatusTimeline(
ctx context.Context,
requester *gtsmodel.Account,
timeline *timeline.StatusTimeline,
cache *timeline.StatusTimeline,
page *paging.Page,
pagePath string,
pageQuery url.Values,
@ -75,13 +75,11 @@ func (p *Processor) getStatusTimeline(
*apimodel.PageableResponse,
gtserror.WithCode,
) {
var (
filters []*gtsmodel.Filter
mutes *usermute.CompiledUserMuteList
)
var err error
var filters []*gtsmodel.Filter
var mutes *usermute.CompiledUserMuteList
if requester != nil {
var err error
// Fetch all filters relevant for requesting account.
filters, err = p.state.DB.GetFiltersForAccountID(ctx,
@ -110,42 +108,73 @@ func (p *Processor) getStatusTimeline(
// input paging cursor.
id.ValidatePage(page)
// Load status page via timeline cache, also
// getting lo, hi values for next, prev pages.
apiStatuses, lo, hi, err := timeline.Load(ctx,
// Returned models and page params.
var apiStatuses []*apimodel.Status
var lo, hi string
// Status page
// to load.
page,
if cache != nil {
// Load status page via timeline cache, also
// getting lo, hi values for next, prev pages.
apiStatuses, lo, hi, err = cache.Load(ctx,
// Caller provided database
// status page loading function.
loadPage,
// Status page
// to load.
page,
// Status load function for cached timeline entries.
func(ids []string) ([]*gtsmodel.Status, error) {
return p.state.DB.GetStatusesByIDs(ctx, ids)
},
// Caller provided database
// status page loading function.
loadPage,
// Filtering function,
// i.e. filter before caching.
filter,
// Status load function for cached timeline entries.
func(ids []string) ([]*gtsmodel.Status, error) {
return p.state.DB.GetStatusesByIDs(ctx, ids)
},
// Filtering function,
// i.e. filter before caching.
filter,
// 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
},
)
} else {
// Load status page without a receiving timeline cache.
// TODO: remove this code path when all support caching.
apiStatuses, lo, hi, err = timeline.LoadStatusTimeline(ctx,
page,
loadPage,
func(ids []string) ([]*gtsmodel.Status, error) {
return p.state.DB.GetStatusesByIDs(ctx, ids)
},
filter,
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
},
)
}
// 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 {
err := gtserror.Newf("error loading timeline: %w", err)
return nil, gtserror.WrapWithCode(http.StatusInternalServerError, err)

View file

@ -1041,9 +1041,7 @@ func (p *clientAPI) DeleteAccountOrUser(ctx context.Context, cMsg *messages.From
p.surface.removeTimelineEntriesByAccount(account.ID)
// Remove any of their cached timelines.
p.state.Caches.Timelines.Public.Delete(account.ID)
p.state.Caches.Timelines.Home.Delete(account.ID)
p.state.Caches.Timelines.Local.Delete(account.ID)
// Get the IDs of all the lists owned by the given account ID.
listIDs, err := p.state.DB.GetListIDsByAccountID(ctx, account.ID)

View file

@ -804,8 +804,6 @@ func (s *Surface) timelineStatusUpdateForTagFollowers(
// deleteStatusFromTimelines completely removes the given status from all timelines.
// It will also stream deletion of the status to all open streams.
func (s *Surface) deleteStatusFromTimelines(ctx context.Context, statusID string) {
s.State.Caches.Timelines.Public.RemoveByStatusIDs(statusID)
s.State.Caches.Timelines.Local.RemoveByStatusIDs(statusID)
s.State.Caches.Timelines.Home.RemoveByStatusIDs(statusID)
s.State.Caches.Timelines.List.RemoveByStatusIDs(statusID)
s.Stream.Delete(ctx, statusID)
@ -816,17 +814,13 @@ func (s *Surface) deleteStatusFromTimelines(ctx context.Context, statusID string
// stats, boost counts, etc) next time it's fetched by the timeline owner. This goes
// both for the status itself, and for any boosts of the status.
func (s *Surface) invalidateStatusFromTimelines(statusID string) {
s.State.Caches.Timelines.Public.UnprepareByStatusIDs(statusID)
s.State.Caches.Timelines.Local.UnprepareByStatusIDs(statusID)
s.State.Caches.Timelines.Home.UnprepareByStatusIDs(statusID)
s.State.Caches.Timelines.List.UnprepareByStatusIDs(statusID)
}
// removeTimelineEntriesByAccount removes all cached timeline entries authored by account ID.
func (s *Surface) removeTimelineEntriesByAccount(accountID string) {
s.State.Caches.Timelines.Public.RemoveByAccountIDs(accountID)
s.State.Caches.Timelines.Home.RemoveByAccountIDs(accountID)
s.State.Caches.Timelines.Local.RemoveByAccountIDs(accountID)
s.State.Caches.Timelines.List.RemoveByAccountIDs(accountID)
}
@ -835,9 +829,7 @@ func (s *Surface) invalidateTimelinesForAccount(ctx context.Context, accountID s
// There's a lot of visibility changes to caclculate for any
// relationship change, so just clear all account's timelines.
s.State.Caches.Timelines.Public.Clear(accountID)
s.State.Caches.Timelines.Home.Clear(accountID)
s.State.Caches.Timelines.Local.Clear(accountID)
// Get the IDs of all the lists owned by the given account ID.
listIDs, err := s.State.DB.GetListIDsByAccountID(ctx, accountID)