diff options
author | 2023-07-31 11:25:29 +0100 | |
---|---|---|
committer | 2023-07-31 11:25:29 +0100 | |
commit | ed2477ebea4c3ceec5949821f4950db9669a4a15 (patch) | |
tree | 1038d7abdfc787ddfc1febb326fd38775b189b85 /internal/cache | |
parent | [bugfix/frontend] Decode URI component domain before showing on frontend (#2043) (diff) | |
download | gotosocial-ed2477ebea4c3ceec5949821f4950db9669a4a15.tar.xz |
[performance] cache follow, follow request and block ID lists (#2027)
Diffstat (limited to 'internal/cache')
-rw-r--r-- | internal/cache/cache.go | 80 | ||||
-rw-r--r-- | internal/cache/gts.go | 150 | ||||
-rw-r--r-- | internal/cache/slice.go | 76 | ||||
-rw-r--r-- | internal/cache/util.go | 23 |
4 files changed, 285 insertions, 44 deletions
diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 63564935e..e97dce6f9 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -80,6 +80,27 @@ func (c *Caches) setuphooks() { // Invalidate account ID cached visibility. c.Visibility.Invalidate("ItemID", account.ID) c.Visibility.Invalidate("RequesterID", account.ID) + + // Invalidate this account's + // following / follower lists. + // (see FollowIDs() comment for details). + c.GTS.FollowIDs().InvalidateAll( + ">"+account.ID, + "l>"+account.ID, + "<"+account.ID, + "l<"+account.ID, + ) + + // Invalidate this account's + // follow requesting / request lists. + // (see FollowRequestIDs() comment for details). + c.GTS.FollowRequestIDs().InvalidateAll( + ">"+account.ID, + "<"+account.ID, + ) + + // Invalidate this account's block lists. + c.GTS.BlockIDs().Invalidate(account.ID) }) c.GTS.Block().SetInvalidateCallback(func(block *gtsmodel.Block) { @@ -90,6 +111,9 @@ func (c *Caches) setuphooks() { // Invalidate block target account ID cached visibility. c.Visibility.Invalidate("ItemID", block.TargetAccountID) c.Visibility.Invalidate("RequesterID", block.TargetAccountID) + + // Invalidate source account's block lists. + c.GTS.BlockIDs().Invalidate(block.AccountID) }) c.GTS.EmojiCategory().SetInvalidateCallback(func(category *gtsmodel.EmojiCategory) { @@ -98,6 +122,9 @@ func (c *Caches) setuphooks() { }) c.GTS.Follow().SetInvalidateCallback(func(follow *gtsmodel.Follow) { + // Invalidate follow request with this same ID. + c.GTS.FollowRequest().Invalidate("ID", follow.ID) + // Invalidate any related list entries. c.GTS.ListEntry().Invalidate("FollowID", follow.ID) @@ -108,19 +135,35 @@ func (c *Caches) setuphooks() { // Invalidate follow target account ID cached visibility. c.Visibility.Invalidate("ItemID", follow.TargetAccountID) c.Visibility.Invalidate("RequesterID", follow.TargetAccountID) + + // Invalidate source account's following + // lists, and destination's follwer lists. + // (see FollowIDs() comment for details). + c.GTS.FollowIDs().InvalidateAll( + ">"+follow.AccountID, + "l>"+follow.AccountID, + "<"+follow.AccountID, + "l<"+follow.AccountID, + "<"+follow.TargetAccountID, + "l<"+follow.TargetAccountID, + ">"+follow.TargetAccountID, + "l>"+follow.TargetAccountID, + ) }) c.GTS.FollowRequest().SetInvalidateCallback(func(followReq *gtsmodel.FollowRequest) { - // Invalidate follow request origin account ID cached visibility. - c.Visibility.Invalidate("ItemID", followReq.AccountID) - c.Visibility.Invalidate("RequesterID", followReq.AccountID) - - // Invalidate follow request target account ID cached visibility. - c.Visibility.Invalidate("ItemID", followReq.TargetAccountID) - c.Visibility.Invalidate("RequesterID", followReq.TargetAccountID) - - // Invalidate any cached follow with same ID. + // Invalidate follow with this same ID. c.GTS.Follow().Invalidate("ID", followReq.ID) + + // Invalidate source account's followreq + // lists, and destinations follow req lists. + // (see FollowRequestIDs() comment for details). + c.GTS.FollowRequestIDs().InvalidateAll( + ">"+followReq.AccountID, + "<"+followReq.AccountID, + ">"+followReq.TargetAccountID, + "<"+followReq.TargetAccountID, + ) }) c.GTS.List().SetInvalidateCallback(func(list *gtsmodel.List) { @@ -128,12 +171,29 @@ func (c *Caches) setuphooks() { c.GTS.ListEntry().Invalidate("ListID", list.ID) }) + c.GTS.Media().SetInvalidateCallback(func(media *gtsmodel.MediaAttachment) { + if *media.Avatar || *media.Header { + // Invalidate cache of attaching account. + c.GTS.Account().Invalidate("ID", media.AccountID) + } + + if media.StatusID != "" { + // Invalidate cache of attaching status. + c.GTS.Status().Invalidate("ID", media.StatusID) + } + }) + c.GTS.Status().SetInvalidateCallback(func(status *gtsmodel.Status) { // Invalidate status ID cached visibility. c.Visibility.Invalidate("ItemID", status.ID) for _, id := range status.AttachmentIDs { - // Invalidate cache for attached media IDs, + // Invalidate each media by the IDs we're aware of. + // This must be done as the status table is aware of + // the media IDs in use before the media table is + // aware of the status ID they are linked to. + // + // c.GTS.Media().Invalidate("StatusID") will not work. c.GTS.Media().Invalidate("ID", id) } }) diff --git a/internal/cache/gts.go b/internal/cache/gts.go index dd43154ef..fefd02fff 100644 --- a/internal/cache/gts.go +++ b/internal/cache/gts.go @@ -26,29 +26,31 @@ import ( ) type GTSCaches struct { - account *result.Cache[*gtsmodel.Account] - accountNote *result.Cache[*gtsmodel.AccountNote] - block *result.Cache[*gtsmodel.Block] - // TODO: maybe should be moved out of here since it's - // not actually doing anything with gtsmodel.DomainBlock. - domainBlock *domain.BlockCache - emoji *result.Cache[*gtsmodel.Emoji] - emojiCategory *result.Cache[*gtsmodel.EmojiCategory] - follow *result.Cache[*gtsmodel.Follow] - followRequest *result.Cache[*gtsmodel.FollowRequest] - instance *result.Cache[*gtsmodel.Instance] - list *result.Cache[*gtsmodel.List] - listEntry *result.Cache[*gtsmodel.ListEntry] - marker *result.Cache[*gtsmodel.Marker] - media *result.Cache[*gtsmodel.MediaAttachment] - mention *result.Cache[*gtsmodel.Mention] - notification *result.Cache[*gtsmodel.Notification] - report *result.Cache[*gtsmodel.Report] - status *result.Cache[*gtsmodel.Status] - statusFave *result.Cache[*gtsmodel.StatusFave] - tombstone *result.Cache[*gtsmodel.Tombstone] - user *result.Cache[*gtsmodel.User] - // TODO: move out of GTS caches since not using database models. + account *result.Cache[*gtsmodel.Account] + accountNote *result.Cache[*gtsmodel.AccountNote] + block *result.Cache[*gtsmodel.Block] + blockIDs *SliceCache[string] + domainBlock *domain.BlockCache + emoji *result.Cache[*gtsmodel.Emoji] + emojiCategory *result.Cache[*gtsmodel.EmojiCategory] + follow *result.Cache[*gtsmodel.Follow] + followIDs *SliceCache[string] + followRequest *result.Cache[*gtsmodel.FollowRequest] + followRequestIDs *SliceCache[string] + instance *result.Cache[*gtsmodel.Instance] + list *result.Cache[*gtsmodel.List] + listEntry *result.Cache[*gtsmodel.ListEntry] + marker *result.Cache[*gtsmodel.Marker] + media *result.Cache[*gtsmodel.MediaAttachment] + mention *result.Cache[*gtsmodel.Mention] + notification *result.Cache[*gtsmodel.Notification] + report *result.Cache[*gtsmodel.Report] + status *result.Cache[*gtsmodel.Status] + statusFave *result.Cache[*gtsmodel.StatusFave] + tombstone *result.Cache[*gtsmodel.Tombstone] + user *result.Cache[*gtsmodel.User] + + // TODO: move out of GTS caches since unrelated to DB. webfinger *ttl.Cache[string, string] } @@ -58,11 +60,14 @@ func (c *GTSCaches) Init() { c.initAccount() c.initAccountNote() c.initBlock() + c.initBlockIDs() c.initDomainBlock() c.initEmoji() c.initEmojiCategory() c.initFollow() + c.initFollowIDs() c.initFollowRequest() + c.initFollowRequestIDs() c.initInstance() c.initList() c.initListEntry() @@ -83,10 +88,28 @@ func (c *GTSCaches) Start() { tryStart(c.account, config.GetCacheGTSAccountSweepFreq()) tryStart(c.accountNote, config.GetCacheGTSAccountNoteSweepFreq()) tryStart(c.block, config.GetCacheGTSBlockSweepFreq()) + tryUntil("starting block IDs cache", 5, func() bool { + if sweep := config.GetCacheGTSBlockIDsSweepFreq(); sweep > 0 { + return c.blockIDs.Start(sweep) + } + return true + }) tryStart(c.emoji, config.GetCacheGTSEmojiSweepFreq()) tryStart(c.emojiCategory, config.GetCacheGTSEmojiCategorySweepFreq()) tryStart(c.follow, config.GetCacheGTSFollowSweepFreq()) + tryUntil("starting follow IDs cache", 5, func() bool { + if sweep := config.GetCacheGTSFollowIDsSweepFreq(); sweep > 0 { + return c.followIDs.Start(sweep) + } + return true + }) tryStart(c.followRequest, config.GetCacheGTSFollowRequestSweepFreq()) + tryUntil("starting follow request IDs cache", 5, func() bool { + if sweep := config.GetCacheGTSFollowRequestIDsSweepFreq(); sweep > 0 { + return c.followRequestIDs.Start(sweep) + } + return true + }) tryStart(c.instance, config.GetCacheGTSInstanceSweepFreq()) tryStart(c.list, config.GetCacheGTSListSweepFreq()) tryStart(c.listEntry, config.GetCacheGTSListEntrySweepFreq()) @@ -112,10 +135,28 @@ func (c *GTSCaches) Stop() { tryStop(c.account, config.GetCacheGTSAccountSweepFreq()) tryStop(c.accountNote, config.GetCacheGTSAccountNoteSweepFreq()) tryStop(c.block, config.GetCacheGTSBlockSweepFreq()) + tryUntil("stopping block IDs cache", 5, func() bool { + if config.GetCacheGTSBlockIDsSweepFreq() > 0 { + return c.blockIDs.Stop() + } + return true + }) tryStop(c.emoji, config.GetCacheGTSEmojiSweepFreq()) tryStop(c.emojiCategory, config.GetCacheGTSEmojiCategorySweepFreq()) tryStop(c.follow, config.GetCacheGTSFollowSweepFreq()) + tryUntil("stopping follow IDs cache", 5, func() bool { + if config.GetCacheGTSFollowIDsSweepFreq() > 0 { + return c.followIDs.Stop() + } + return true + }) tryStop(c.followRequest, config.GetCacheGTSFollowRequestSweepFreq()) + tryUntil("stopping follow request IDs cache", 5, func() bool { + if config.GetCacheGTSFollowRequestIDsSweepFreq() > 0 { + return c.followRequestIDs.Stop() + } + return true + }) tryStop(c.instance, config.GetCacheGTSInstanceSweepFreq()) tryStop(c.list, config.GetCacheGTSListSweepFreq()) tryStop(c.listEntry, config.GetCacheGTSListEntrySweepFreq()) @@ -128,7 +169,12 @@ func (c *GTSCaches) Stop() { tryStop(c.statusFave, config.GetCacheGTSStatusFaveSweepFreq()) tryStop(c.tombstone, config.GetCacheGTSTombstoneSweepFreq()) tryStop(c.user, config.GetCacheGTSUserSweepFreq()) - tryUntil("stopping *gtsmodel.Webfinger cache", 5, c.webfinger.Stop) + tryUntil("stopping *gtsmodel.Webfinger cache", 5, func() bool { + if config.GetCacheGTSWebfingerSweepFreq() > 0 { + return c.webfinger.Stop() + } + return true + }) } // Account provides access to the gtsmodel Account database cache. @@ -146,6 +192,11 @@ func (c *GTSCaches) Block() *result.Cache[*gtsmodel.Block] { return c.block } +// FollowIDs provides access to the block IDs database cache. +func (c *GTSCaches) BlockIDs() *SliceCache[string] { + return c.blockIDs +} + // DomainBlock provides access to the domain block database cache. func (c *GTSCaches) DomainBlock() *domain.BlockCache { return c.domainBlock @@ -166,11 +217,29 @@ func (c *GTSCaches) Follow() *result.Cache[*gtsmodel.Follow] { return c.follow } +// FollowIDs provides access to the follower / following IDs database cache. +// THIS CACHE IS KEYED AS THE FOLLOWING {prefix}{accountID} WHERE PREFIX IS: +// - '>' for following IDs +// - 'l>' for local following IDs +// - '<' for follower IDs +// - 'l<' for local follower IDs +func (c *GTSCaches) FollowIDs() *SliceCache[string] { + return c.followIDs +} + // FollowRequest provides access to the gtsmodel FollowRequest database cache. func (c *GTSCaches) FollowRequest() *result.Cache[*gtsmodel.FollowRequest] { return c.followRequest } +// FollowRequestIDs provides access to the follow requester / requesting IDs database +// cache. THIS CACHE IS KEYED AS THE FOLLOWING {prefix}{accountID} WHERE PREFIX IS: +// - '>' for following IDs +// - '<' for follower IDs +func (c *GTSCaches) FollowRequestIDs() *SliceCache[string] { + return c.followRequestIDs +} + // Instance provides access to the gtsmodel Instance database cache. func (c *GTSCaches) Instance() *result.Cache[*gtsmodel.Instance] { return c.instance @@ -274,6 +343,8 @@ func (c *GTSCaches) initBlock() { {Name: "ID"}, {Name: "URI"}, {Name: "AccountID.TargetAccountID"}, + {Name: "AccountID", Multi: true}, + {Name: "TargetAccountID", Multi: true}, }, func(b1 *gtsmodel.Block) *gtsmodel.Block { b2 := new(gtsmodel.Block) *b2 = *b1 @@ -283,6 +354,14 @@ func (c *GTSCaches) initBlock() { c.block.IgnoreErrors(ignoreErrors) } +func (c *GTSCaches) initBlockIDs() { + c.blockIDs = &SliceCache[string]{Cache: ttl.New[string, []string]( + 0, + config.GetCacheGTSBlockIDsMaxSize(), + config.GetCacheGTSBlockIDsTTL(), + )} +} + func (c *GTSCaches) initDomainBlock() { c.domainBlock = new(domain.BlockCache) } @@ -321,6 +400,8 @@ func (c *GTSCaches) initFollow() { {Name: "ID"}, {Name: "URI"}, {Name: "AccountID.TargetAccountID"}, + {Name: "AccountID", Multi: true}, + {Name: "TargetAccountID", Multi: true}, }, func(f1 *gtsmodel.Follow) *gtsmodel.Follow { f2 := new(gtsmodel.Follow) *f2 = *f1 @@ -329,11 +410,21 @@ func (c *GTSCaches) initFollow() { c.follow.SetTTL(config.GetCacheGTSFollowTTL(), true) } +func (c *GTSCaches) initFollowIDs() { + c.followIDs = &SliceCache[string]{Cache: ttl.New[string, []string]( + 0, + config.GetCacheGTSFollowIDsMaxSize(), + config.GetCacheGTSFollowIDsTTL(), + )} +} + func (c *GTSCaches) initFollowRequest() { c.followRequest = result.New([]result.Lookup{ {Name: "ID"}, {Name: "URI"}, {Name: "AccountID.TargetAccountID"}, + {Name: "AccountID", Multi: true}, + {Name: "TargetAccountID", Multi: true}, }, func(f1 *gtsmodel.FollowRequest) *gtsmodel.FollowRequest { f2 := new(gtsmodel.FollowRequest) *f2 = *f1 @@ -342,6 +433,14 @@ func (c *GTSCaches) initFollowRequest() { c.followRequest.SetTTL(config.GetCacheGTSFollowRequestTTL(), true) } +func (c *GTSCaches) initFollowRequestIDs() { + c.followRequestIDs = &SliceCache[string]{Cache: ttl.New[string, []string]( + 0, + config.GetCacheGTSFollowRequestIDsMaxSize(), + config.GetCacheGTSFollowRequestIDsTTL(), + )} +} + func (c *GTSCaches) initInstance() { c.instance = result.New([]result.Lookup{ {Name: "ID"}, @@ -502,5 +601,6 @@ func (c *GTSCaches) initWebfinger() { c.webfinger = ttl.New[string, string]( 0, config.GetCacheGTSWebfingerMaxSize(), - config.GetCacheGTSWebfingerTTL()) + config.GetCacheGTSWebfingerTTL(), + ) } diff --git a/internal/cache/slice.go b/internal/cache/slice.go new file mode 100644 index 000000000..194f20d4b --- /dev/null +++ b/internal/cache/slice.go @@ -0,0 +1,76 @@ +// 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 cache + +import ( + "codeberg.org/gruf/go-cache/v3/ttl" + "golang.org/x/exp/slices" +) + +// SliceCache wraps a ttl.Cache to provide simple loader-callback +// functions for fetching + caching slices of objects (e.g. IDs). +type SliceCache[T any] struct { + *ttl.Cache[string, []T] +} + +// Load will attempt to load an existing slice from the cache for the given key, else calling the provided load function and caching the result. +func (c *SliceCache[T]) Load(key string, load func() ([]T, error)) ([]T, error) { + // Look for follow IDs list in cache under this key. + data, ok := c.Get(key) + + if !ok { + var err error + + // Not cached, load! + data, err = load() + if err != nil { + return nil, err + } + + // Store the data. + c.Set(key, data) + } + + // Return data clone for safety. + return slices.Clone(data), nil +} + +// LoadRange is functionally the same as .Load(), but will pass the result through provided reslice function before returning a cloned result. +func (c *SliceCache[T]) LoadRange(key string, load func() ([]T, error), reslice func([]T) []T) ([]T, error) { + // Look for follow IDs list in cache under this key. + data, ok := c.Get(key) + + if !ok { + var err error + + // Not cached, load! + data, err = load() + if err != nil { + return nil, err + } + + // Store the data. + c.Set(key, data) + } + + // Reslice to range. + slice := reslice(data) + + // Return range clone for safety. + return slices.Clone(slice), nil +} diff --git a/internal/cache/util.go b/internal/cache/util.go index a0adfd366..f2357c904 100644 --- a/internal/cache/util.go +++ b/internal/cache/util.go @@ -18,28 +18,33 @@ package cache import ( - "context" + "database/sql" "errors" "fmt" "time" "codeberg.org/gruf/go-cache/v3/result" errorsv2 "codeberg.org/gruf/go-errors/v2" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/log" ) -// SentinelError is returned to indicate a non-permanent error return, -// i.e. a situation in which we do not want a cache a negative result. +// SentinelError is an error that can be returned and checked against to indicate a non-permanent +// error return from a cache loader callback, e.g. a temporary situation that will soon be fixed. var SentinelError = errors.New("BUG: error should not be returned") //nolint:revive -// ignoreErrors is an error ignoring function capable of being passed to -// caches, which specifically catches and ignores our sentinel error type. +// ignoreErrors is an error matching function used to signal which errors +// the result caches should NOT hold onto. these amount to anything non-permanent. func ignoreErrors(err error) bool { - return errorsv2.Comparable( + return !errorsv2.Comparable( err, - SentinelError, - context.DeadlineExceeded, - context.Canceled, + + // the only cacheable errs, + // i.e anything permanent + // (until invalidation). + db.ErrNoEntries, + db.ErrAlreadyExists, + sql.ErrNoRows, ) } |