summaryrefslogtreecommitdiff
path: root/internal/cache
diff options
context:
space:
mode:
authorLibravatar kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com>2023-07-31 11:25:29 +0100
committerLibravatar GitHub <noreply@github.com>2023-07-31 11:25:29 +0100
commited2477ebea4c3ceec5949821f4950db9669a4a15 (patch)
tree1038d7abdfc787ddfc1febb326fd38775b189b85 /internal/cache
parent[bugfix/frontend] Decode URI component domain before showing on frontend (#2043) (diff)
downloadgotosocial-ed2477ebea4c3ceec5949821f4950db9669a4a15.tar.xz
[performance] cache follow, follow request and block ID lists (#2027)
Diffstat (limited to 'internal/cache')
-rw-r--r--internal/cache/cache.go80
-rw-r--r--internal/cache/gts.go150
-rw-r--r--internal/cache/slice.go76
-rw-r--r--internal/cache/util.go23
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,
)
}