diff options
Diffstat (limited to 'internal/timeline')
| -rw-r--r-- | internal/timeline/manager.go | 35 | ||||
| -rw-r--r-- | internal/timeline/timeline.go | 16 | ||||
| -rw-r--r-- | internal/timeline/unprepare.go | 50 | ||||
| -rw-r--r-- | internal/timeline/unprepare_test.go | 142 | 
4 files changed, 239 insertions, 4 deletions
diff --git a/internal/timeline/manager.go b/internal/timeline/manager.go index 95a40aca1..a701756bb 100644 --- a/internal/timeline/manager.go +++ b/internal/timeline/manager.go @@ -75,6 +75,14 @@ type Manager interface {  	// WipeStatusesFromAccountID removes all items by the given accountID from the given timeline.  	WipeItemsFromAccountID(ctx context.Context, timelineID string, accountID string) error +	// UnprepareItem unprepares/uncaches the prepared version fo the given itemID from the given timelineID. +	// Use this for cache invalidation when the prepared representation of an item has changed. +	UnprepareItem(ctx context.Context, timelineID string, itemID string) error + +	// UnprepareItemFromAllTimelines unprepares/uncaches the prepared version of the given itemID from all timelines. +	// Use this for cache invalidation when the prepared representation of an item has changed. +	UnprepareItemFromAllTimelines(ctx context.Context, itemID string) error +  	// Prune manually triggers a prune operation for the given timelineID.  	Prune(ctx context.Context, timelineID string, desiredPreparedItemsLength int, desiredIndexedItemsLength int) (int, error) @@ -193,7 +201,7 @@ func (m *manager) WipeItemFromAllTimelines(ctx context.Context, itemID string) e  	})  	if len(errors) > 0 { -		return gtserror.Newf("one or more errors wiping status %s: %w", itemID, errors.Combine()) +		return gtserror.Newf("error(s) wiping status %s: %w", itemID, errors.Combine())  	}  	return nil @@ -204,6 +212,31 @@ func (m *manager) WipeItemsFromAccountID(ctx context.Context, timelineID string,  	return err  } +func (m *manager) UnprepareItemFromAllTimelines(ctx context.Context, itemID string) error { +	errors := gtserror.MultiError{} + +	// Work through all timelines held by this +	// manager, and call Unprepare for each. +	m.timelines.Range(func(_ any, v any) bool { +		// nolint:forcetypeassert +		if err := v.(Timeline).Unprepare(ctx, itemID); err != nil { +			errors.Append(err) +		} + +		return true // always continue range +	}) + +	if len(errors) > 0 { +		return gtserror.Newf("error(s) unpreparing status %s: %w", itemID, errors.Combine()) +	} + +	return nil +} + +func (m *manager) UnprepareItem(ctx context.Context, timelineID string, itemID string) error { +	return m.getOrCreateTimeline(ctx, timelineID).Unprepare(ctx, itemID) +} +  func (m *manager) Prune(ctx context.Context, timelineID string, desiredPreparedItemsLength int, desiredIndexedItemsLength int) (int, error) {  	return m.getOrCreateTimeline(ctx, timelineID).Prune(desiredPreparedItemsLength, desiredIndexedItemsLength), nil  } diff --git a/internal/timeline/timeline.go b/internal/timeline/timeline.go index b973a3905..e7c609638 100644 --- a/internal/timeline/timeline.go +++ b/internal/timeline/timeline.go @@ -78,12 +78,22 @@ type Timeline interface {  		INDEXING + PREPARATION FUNCTIONS  	*/ -	// IndexAndPrepareOne puts a item into the timeline at the appropriate place according to its id, and then immediately prepares it. +	// IndexAndPrepareOne puts a item into the timeline at the appropriate place +	// according to its id, and then immediately prepares it.  	// -	// The returned bool indicates whether or not the item was actually inserted into the timeline. This will be false -	// if the item is a boost and the original item or another boost of it already exists < boostReinsertionDepth back in the timeline. +	// The returned bool indicates whether or not the item was actually inserted +	// into the timeline. This will be false if the item is a boost and the original +	// item, or a boost of it, already exists recently in the timeline.  	IndexAndPrepareOne(ctx context.Context, itemID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) +	// Unprepare clears the prepared version of the given item (and any boosts +	// thereof) from the timeline, but leaves the indexed version in place. +	// +	// This is useful for cache invalidation when the prepared version of the +	// item has changed for some reason (edits, updates, etc), but the item does +	// not need to be removed: it will be prepared again next time Get is called. +	Unprepare(ctx context.Context, itemID string) error +  	/*  		INFO FUNCTIONS  	*/ diff --git a/internal/timeline/unprepare.go b/internal/timeline/unprepare.go new file mode 100644 index 000000000..827b274d8 --- /dev/null +++ b/internal/timeline/unprepare.go @@ -0,0 +1,50 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +package timeline + +import ( +	"context" +) + +func (t *timeline) Unprepare(ctx context.Context, itemID string) error { +	t.Lock() +	defer t.Unlock() + +	if t.items == nil || t.items.data == nil { +		// Nothing to do. +		return nil +	} + +	for e := t.items.data.Front(); e != nil; e = e.Next() { +		entry := e.Value.(*indexedItemsEntry) // nolint:forcetypeassert + +		if entry.itemID != itemID && entry.boostOfID != itemID { +			// Not relevant. +			continue +		} + +		if entry.prepared == nil { +			// It's already unprepared (mood). +			continue +		} + +		entry.prepared = nil // <- eat this up please garbage collector nom nom nom +	} + +	return nil +} diff --git a/internal/timeline/unprepare_test.go b/internal/timeline/unprepare_test.go new file mode 100644 index 000000000..20bef7537 --- /dev/null +++ b/internal/timeline/unprepare_test.go @@ -0,0 +1,142 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +package timeline_test + +import ( +	"context" +	"testing" + +	"github.com/stretchr/testify/suite" + +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/id" +) + +type UnprepareTestSuite struct { +	TimelineStandardTestSuite +} + +func (suite *UnprepareTestSuite) TestUnprepareFromFave() { +	var ( +		ctx         = context.Background() +		testAccount = suite.testAccounts["local_account_1"] +		maxID       = "" +		sinceID     = "" +		minID       = "" +		limit       = 1 +		local       = false +	) + +	suite.fillTimeline(testAccount.ID) + +	// Get first status from the top (no params). +	statuses, err := suite.state.Timelines.Home.GetTimeline( +		ctx, +		testAccount.ID, +		maxID, +		sinceID, +		minID, +		limit, +		local, +	) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	if len(statuses) != 1 { +		suite.FailNow("couldn't get top status") +	} + +	targetStatus := statuses[0].(*apimodel.Status) + +	// Check fave stats of the top status. +	suite.Equal(0, targetStatus.FavouritesCount) +	suite.False(targetStatus.Favourited) + +	// Fave the top status from testAccount. +	if err := suite.state.DB.PutStatusFave(ctx, >smodel.StatusFave{ +		ID:              id.NewULID(), +		AccountID:       testAccount.ID, +		TargetAccountID: targetStatus.Account.ID, +		StatusID:        targetStatus.ID, +		URI:             "https://example.org/some/activity/path", +	}); err != nil { +		suite.FailNow(err.Error()) +	} + +	// Repeat call to get first status from the top. +	// Get first status from the top (no params). +	statuses, err = suite.state.Timelines.Home.GetTimeline( +		ctx, +		testAccount.ID, +		maxID, +		sinceID, +		minID, +		limit, +		local, +	) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	if len(statuses) != 1 { +		suite.FailNow("couldn't get top status") +	} + +	targetStatus = statuses[0].(*apimodel.Status) + +	// We haven't yet uncached/unprepared the status, +	// we've only inserted the fave, so counts should +	// stay the same... +	suite.Equal(0, targetStatus.FavouritesCount) +	suite.False(targetStatus.Favourited) + +	// Now call unprepare. +	suite.state.Timelines.Home.UnprepareItemFromAllTimelines(ctx, targetStatus.ID) + +	// Now a Get should trigger a fresh prepare of the +	// target status, and the counts should be updated. +	// Repeat call to get first status from the top. +	// Get first status from the top (no params). +	statuses, err = suite.state.Timelines.Home.GetTimeline( +		ctx, +		testAccount.ID, +		maxID, +		sinceID, +		minID, +		limit, +		local, +	) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	if len(statuses) != 1 { +		suite.FailNow("couldn't get top status") +	} + +	targetStatus = statuses[0].(*apimodel.Status) + +	suite.Equal(1, targetStatus.FavouritesCount) +	suite.True(targetStatus.Favourited) +} + +func TestUnprepareTestSuite(t *testing.T) { +	suite.Run(t, new(UnprepareTestSuite)) +}  | 
