[performance] refactoring + add fave / follow / request / visibility caching (#1607)

* refactor visibility checking, add caching for visibility

* invalidate visibility cache items on account / status deletes

* fix requester ID passed to visibility cache nil ptr

* de-interface caches, fix home / public timeline caching + visibility

* finish adding code comments for visibility filter

* fix angry goconst linter warnings

* actually finish adding filter visibility code comments for timeline functions

* move home timeline status author check to after visibility

* remove now-unused code

* add more code comments

* add TODO code comment, update printed cache start names

* update printed cache names on stop

* start adding separate follow(request) delete db functions, add specific visibility cache tests

* add relationship type caching

* fix getting local account follows / followed-bys, other small codebase improvements

* simplify invalidation using cache hooks, add more GetAccountBy___() functions

* fix boosting to return 404 if not boostable but no error (to not leak status ID)

* remove dead code

* improved placement of cache invalidation

* update license headers

* add example follow, follow-request config entries

* add example visibility cache configuration to config file

* use specific PutFollowRequest() instead of just Put()

* add tests for all GetAccountBy()

* add GetBlockBy() tests

* update block to check primitive fields

* update and finish adding Get{Account,Block,Follow,FollowRequest}By() tests

* fix copy-pasted code

* update envparsing test

* whitespace

* fix bun struct tag

* add license header to gtscontext

* fix old license header

* improved error creation to not use fmt.Errorf() when not needed

* fix various rebase conflicts, fix account test

* remove commented-out code, fix-up mention caching

* fix mention select bun statement

* ensure mention target account populated, pass in context to customrenderer logging

* remove more uncommented code, fix typeutil test

* add statusfave database model caching

* add status fave cache configuration

* add status fave cache example config

* woops, catch missed error. nice catch linter!

* add back testrig panic on nil db

* update example configuration to match defaults, slight tweak to cache configuration defaults

* update envparsing test with new defaults

* fetch followingget to use the follow target account

* use accounnt.IsLocal() instead of empty domain check

* use constants for the cache visibility type check

* use bun.In() for notification type restriction in db query

* include replies when fetching PublicTimeline() (to account for single-author threads in Visibility{}.StatusPublicTimelineable())

* use bun query building for nested select statements to ensure working with postgres

* update public timeline future status checks to match visibility filter

* same as previous, for home timeline

* update public timeline tests to dynamically check for appropriate statuses

* migrate accounts to allow unique constraint on public_key

* provide minimal account with publicKey

---------

Signed-off-by: kim <grufwub@gmail.com>
Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
This commit is contained in:
kim
2023-03-28 14:03:14 +01:00
committed by GitHub
parent 7d09863393
commit de6e3e5f2a
100 changed files with 4423 additions and 2367 deletions

29
internal/cache/ap.go vendored
View File

@@ -17,27 +17,14 @@
package cache
type APCaches interface {
// Init will initialize all the ActivityPub caches in this collection.
// NOTE: the cache MUST NOT be in use anywhere, this is not thread-safe.
Init()
type APCaches struct{}
// Start will attempt to start all of the ActivityPub caches, or panic.
Start()
// Init will initialize all the ActivityPub caches in this collection.
// NOTE: the cache MUST NOT be in use anywhere, this is not thread-safe.
func (c *APCaches) Init() {}
// Stop will attempt to stop all of the ActivityPub caches, or panic.
Stop()
}
// Start will attempt to start all of the ActivityPub caches, or panic.
func (c *APCaches) Start() {}
// NewAP returns a new default implementation of APCaches.
func NewAP() APCaches {
return &apCaches{}
}
type apCaches struct{}
func (c *apCaches) Init() {}
func (c *apCaches) Start() {}
func (c *apCaches) Stop() {}
// Stop will attempt to stop all of the ActivityPub caches, or panic.
func (c *APCaches) Stop() {}

View File

@@ -17,13 +17,23 @@
package cache
import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
type Caches struct {
// GTS provides access to the collection of gtsmodel object caches.
// (used by the database).
GTS GTSCaches
// AP provides access to the collection of ActivityPub object caches.
// (planned to be used by the typeconverter).
AP APCaches
// Visibility provides access to the item visibility cache.
// (used by the visibility filter).
Visibility VisibilityCache
// prevent pass-by-value.
_ nocopy
}
@@ -31,29 +41,77 @@ type Caches struct {
// Init will (re)initialize both the GTS and AP cache collections.
// NOTE: the cache MUST NOT be in use anywhere, this is not thread-safe.
func (c *Caches) Init() {
if c.GTS == nil {
// use default impl
c.GTS = NewGTS()
}
if c.AP == nil {
// use default impl
c.AP = NewAP()
}
// initialize caches
c.GTS.Init()
c.AP.Init()
c.Visibility.Init()
// Setup cache invalidate hooks.
// !! READ THE METHOD COMMENT
c.setuphooks()
}
// Start will start both the GTS and AP cache collections.
func (c *Caches) Start() {
c.GTS.Start()
c.AP.Start()
c.Visibility.Start()
}
// Stop will stop both the GTS and AP cache collections.
func (c *Caches) Stop() {
c.GTS.Stop()
c.AP.Stop()
c.Visibility.Stop()
}
// setuphooks sets necessary cache invalidation hooks between caches,
// as an invalidation indicates a database UPDATE / DELETE. INSERT is
// not handled by invalidation hooks and must be invalidated manually.
func (c *Caches) setuphooks() {
c.GTS.Account().SetInvalidateCallback(func(account *gtsmodel.Account) {
// Invalidate account ID cached visibility.
c.Visibility.Invalidate("ItemID", account.ID)
c.Visibility.Invalidate("RequesterID", account.ID)
})
c.GTS.Block().SetInvalidateCallback(func(block *gtsmodel.Block) {
// Invalidate block origin account ID cached visibility.
c.Visibility.Invalidate("ItemID", block.AccountID)
c.Visibility.Invalidate("RequesterID", block.AccountID)
// Invalidate block target account ID cached visibility.
c.Visibility.Invalidate("ItemID", block.TargetAccountID)
c.Visibility.Invalidate("RequesterID", block.TargetAccountID)
})
c.GTS.Follow().SetInvalidateCallback(func(follow *gtsmodel.Follow) {
// Invalidate follow origin account ID cached visibility.
c.Visibility.Invalidate("ItemID", follow.AccountID)
c.Visibility.Invalidate("RequesterID", follow.AccountID)
// Invalidate follow target account ID cached visibility.
c.Visibility.Invalidate("ItemID", follow.TargetAccountID)
c.Visibility.Invalidate("RequesterID", 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)
})
c.GTS.Status().SetInvalidateCallback(func(status *gtsmodel.Status) {
// Invalidate status ID cached visibility.
c.Visibility.Invalidate("ItemID", status.ID)
})
c.GTS.User().SetInvalidateCallback(func(user *gtsmodel.User) {
// Invalidate local account ID cached visibility.
c.Visibility.Invalidate("ItemID", user.AccountID)
c.Visibility.Invalidate("RequesterID", user.AccountID)
})
}

309
internal/cache/gts.go vendored
View File

@@ -25,240 +25,221 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
type GTSCaches interface {
// Init will initialize all the gtsmodel caches in this collection.
// NOTE: the cache MUST NOT be in use anywhere, this is not thread-safe.
Init()
// Start will attempt to start all of the gtsmodel caches, or panic.
Start()
// Stop will attempt to stop all of the gtsmodel caches, or panic.
Stop()
// Account provides access to the gtsmodel Account database cache.
Account() *result.Cache[*gtsmodel.Account]
// Block provides access to the gtsmodel Block (account) database cache.
Block() *result.Cache[*gtsmodel.Block]
// DomainBlock provides access to the domain block database cache.
DomainBlock() *domain.BlockCache
// Emoji provides access to the gtsmodel Emoji database cache.
Emoji() *result.Cache[*gtsmodel.Emoji]
// EmojiCategory provides access to the gtsmodel EmojiCategory database cache.
EmojiCategory() *result.Cache[*gtsmodel.EmojiCategory]
// Mention provides access to the gtsmodel Mention database cache.
Mention() *result.Cache[*gtsmodel.Mention]
// Media provides access to the gtsmodel Media database cache.
Media() *result.Cache[*gtsmodel.MediaAttachment]
// Notification provides access to the gtsmodel Notification database cache.
Notification() *result.Cache[*gtsmodel.Notification]
// Report provides access to the gtsmodel Report database cache.
Report() *result.Cache[*gtsmodel.Report]
// Status provides access to the gtsmodel Status database cache.
Status() *result.Cache[*gtsmodel.Status]
// Tombstone provides access to the gtsmodel Tombstone database cache.
Tombstone() *result.Cache[*gtsmodel.Tombstone]
// User provides access to the gtsmodel User database cache.
User() *result.Cache[*gtsmodel.User]
// Webfinger
Webfinger() *ttl.Cache[string, string]
}
// NewGTS returns a new default implementation of GTSCaches.
func NewGTS() GTSCaches {
return &gtsCaches{}
}
type gtsCaches struct {
account *result.Cache[*gtsmodel.Account]
block *result.Cache[*gtsmodel.Block]
type GTSCaches struct {
account *result.Cache[*gtsmodel.Account]
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]
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]
webfinger *ttl.Cache[string, string]
// TODO: move out of GTS caches since not using database models.
webfinger *ttl.Cache[string, string]
}
func (c *gtsCaches) Init() {
// Init will initialize all the gtsmodel caches in this collection.
// NOTE: the cache MUST NOT be in use anywhere, this is not thread-safe.
func (c *GTSCaches) Init() {
c.initAccount()
c.initBlock()
c.initDomainBlock()
c.initEmoji()
c.initEmojiCategory()
c.initFollow()
c.initFollowRequest()
c.initMedia()
c.initMention()
c.initNotification()
c.initReport()
c.initStatus()
c.initStatusFave()
c.initTombstone()
c.initUser()
c.initWebfinger()
}
func (c *gtsCaches) Start() {
tryUntil("starting gtsmodel.Account cache", 5, func() bool {
return c.account.Start(config.GetCacheGTSAccountSweepFreq())
// Start will attempt to start all of the gtsmodel caches, or panic.
func (c *GTSCaches) Start() {
tryStart(c.account, config.GetCacheGTSAccountSweepFreq())
tryStart(c.block, config.GetCacheGTSBlockSweepFreq())
tryUntil("starting domain block cache", 5, func() bool {
if sweep := config.GetCacheGTSDomainBlockSweepFreq(); sweep > 0 {
return c.domainBlock.Start(sweep)
}
return true
})
tryUntil("starting gtsmodel.Block cache", 5, func() bool {
return c.block.Start(config.GetCacheGTSBlockSweepFreq())
})
tryUntil("starting gtsmodel.DomainBlock cache", 5, func() bool {
return c.domainBlock.Start(config.GetCacheGTSDomainBlockSweepFreq())
})
tryUntil("starting gtsmodel.Emoji cache", 5, func() bool {
return c.emoji.Start(config.GetCacheGTSEmojiSweepFreq())
})
tryUntil("starting gtsmodel.EmojiCategory cache", 5, func() bool {
return c.emojiCategory.Start(config.GetCacheGTSEmojiCategorySweepFreq())
})
tryUntil("starting gtsmodel.MediaAttachment cache", 5, func() bool {
return c.media.Start(config.GetCacheGTSMediaSweepFreq())
})
tryUntil("starting gtsmodel.Mention cache", 5, func() bool {
return c.mention.Start(config.GetCacheGTSMentionSweepFreq())
})
tryUntil("starting gtsmodel.Notification cache", 5, func() bool {
return c.notification.Start(config.GetCacheGTSNotificationSweepFreq())
})
tryUntil("starting gtsmodel.Report cache", 5, func() bool {
return c.report.Start(config.GetCacheGTSReportSweepFreq())
})
tryUntil("starting gtsmodel.Status cache", 5, func() bool {
return c.status.Start(config.GetCacheGTSStatusSweepFreq())
})
tryUntil("starting gtsmodel.Tombstone cache", 5, func() bool {
return c.tombstone.Start(config.GetCacheGTSTombstoneSweepFreq())
})
tryUntil("starting gtsmodel.User cache", 5, func() bool {
return c.user.Start(config.GetCacheGTSUserSweepFreq())
})
tryUntil("starting gtsmodel.Webfinger cache", 5, func() bool {
return c.webfinger.Start(config.GetCacheGTSWebfingerSweepFreq())
tryStart(c.emoji, config.GetCacheGTSEmojiSweepFreq())
tryStart(c.emojiCategory, config.GetCacheGTSEmojiCategorySweepFreq())
tryStart(c.follow, config.GetCacheGTSFollowSweepFreq())
tryStart(c.followRequest, config.GetCacheGTSFollowRequestSweepFreq())
tryStart(c.media, config.GetCacheGTSMediaSweepFreq())
tryStart(c.mention, config.GetCacheGTSMentionSweepFreq())
tryStart(c.notification, config.GetCacheGTSNotificationSweepFreq())
tryStart(c.report, config.GetCacheGTSReportSweepFreq())
tryStart(c.status, config.GetCacheGTSStatusSweepFreq())
tryStart(c.statusFave, config.GetCacheGTSStatusFaveSweepFreq())
tryStart(c.tombstone, config.GetCacheGTSTombstoneSweepFreq())
tryStart(c.user, config.GetCacheGTSUserSweepFreq())
tryUntil("starting *gtsmodel.Webfinger cache", 5, func() bool {
if sweep := config.GetCacheGTSWebfingerSweepFreq(); sweep > 0 {
return c.webfinger.Start(sweep)
}
return true
})
}
func (c *gtsCaches) Stop() {
tryUntil("stopping gtsmodel.Account cache", 5, c.account.Stop)
tryUntil("stopping gtsmodel.Block cache", 5, c.block.Stop)
tryUntil("stopping gtsmodel.DomainBlock cache", 5, c.domainBlock.Stop)
tryUntil("stopping gtsmodel.Emoji cache", 5, c.emoji.Stop)
tryUntil("stopping gtsmodel.EmojiCategory cache", 5, c.emojiCategory.Stop)
tryUntil("stopping gtsmodel.MediaAttachment cache", 5, c.media.Stop)
tryUntil("stopping gtsmodel.Mention cache", 5, c.mention.Stop)
tryUntil("stopping gtsmodel.Notification cache", 5, c.notification.Stop)
tryUntil("stopping gtsmodel.Report cache", 5, c.report.Stop)
tryUntil("stopping gtsmodel.Status cache", 5, c.status.Stop)
tryUntil("stopping gtsmodel.Tombstone cache", 5, c.tombstone.Stop)
tryUntil("stopping gtsmodel.User cache", 5, c.user.Stop)
tryUntil("stopping gtsmodel.Webfinger cache", 5, c.webfinger.Stop)
// Stop will attempt to stop all of the gtsmodel caches, or panic.
func (c *GTSCaches) Stop() {
tryStop(c.account, config.GetCacheGTSAccountSweepFreq())
tryStop(c.block, config.GetCacheGTSBlockSweepFreq())
tryUntil("stopping domain block cache", 5, c.domainBlock.Stop)
tryStop(c.emoji, config.GetCacheGTSEmojiSweepFreq())
tryStop(c.emojiCategory, config.GetCacheGTSEmojiCategorySweepFreq())
tryStop(c.follow, config.GetCacheGTSFollowSweepFreq())
tryStop(c.followRequest, config.GetCacheGTSFollowRequestSweepFreq())
tryStop(c.media, config.GetCacheGTSMediaSweepFreq())
tryStop(c.mention, config.GetCacheGTSNotificationSweepFreq())
tryStop(c.notification, config.GetCacheGTSNotificationSweepFreq())
tryStop(c.report, config.GetCacheGTSReportSweepFreq())
tryStop(c.status, config.GetCacheGTSStatusSweepFreq())
tryStop(c.statusFave, config.GetCacheGTSStatusFaveSweepFreq())
tryStop(c.tombstone, config.GetCacheGTSTombstoneSweepFreq())
tryStop(c.user, config.GetCacheGTSUserSweepFreq())
tryUntil("stopping *gtsmodel.Webfinger cache", 5, c.webfinger.Stop)
}
func (c *gtsCaches) Account() *result.Cache[*gtsmodel.Account] {
// Account provides access to the gtsmodel Account database cache.
func (c *GTSCaches) Account() *result.Cache[*gtsmodel.Account] {
return c.account
}
func (c *gtsCaches) Block() *result.Cache[*gtsmodel.Block] {
// Block provides access to the gtsmodel Block (account) database cache.
func (c *GTSCaches) Block() *result.Cache[*gtsmodel.Block] {
return c.block
}
func (c *gtsCaches) DomainBlock() *domain.BlockCache {
// DomainBlock provides access to the domain block database cache.
func (c *GTSCaches) DomainBlock() *domain.BlockCache {
return c.domainBlock
}
func (c *gtsCaches) Emoji() *result.Cache[*gtsmodel.Emoji] {
// Emoji provides access to the gtsmodel Emoji database cache.
func (c *GTSCaches) Emoji() *result.Cache[*gtsmodel.Emoji] {
return c.emoji
}
func (c *gtsCaches) EmojiCategory() *result.Cache[*gtsmodel.EmojiCategory] {
// EmojiCategory provides access to the gtsmodel EmojiCategory database cache.
func (c *GTSCaches) EmojiCategory() *result.Cache[*gtsmodel.EmojiCategory] {
return c.emojiCategory
}
func (c *gtsCaches) Media() *result.Cache[*gtsmodel.MediaAttachment] {
// Follow provides access to the gtsmodel Follow database cache.
func (c *GTSCaches) Follow() *result.Cache[*gtsmodel.Follow] {
return c.follow
}
// FollowRequest provides access to the gtsmodel FollowRequest database cache.
func (c *GTSCaches) FollowRequest() *result.Cache[*gtsmodel.FollowRequest] {
return c.followRequest
}
// Media provides access to the gtsmodel Media database cache.
func (c *GTSCaches) Media() *result.Cache[*gtsmodel.MediaAttachment] {
return c.media
}
func (c *gtsCaches) Mention() *result.Cache[*gtsmodel.Mention] {
// Mention provides access to the gtsmodel Mention database cache.
func (c *GTSCaches) Mention() *result.Cache[*gtsmodel.Mention] {
return c.mention
}
func (c *gtsCaches) Notification() *result.Cache[*gtsmodel.Notification] {
// Notification provides access to the gtsmodel Notification database cache.
func (c *GTSCaches) Notification() *result.Cache[*gtsmodel.Notification] {
return c.notification
}
func (c *gtsCaches) Report() *result.Cache[*gtsmodel.Report] {
// Report provides access to the gtsmodel Report database cache.
func (c *GTSCaches) Report() *result.Cache[*gtsmodel.Report] {
return c.report
}
func (c *gtsCaches) Status() *result.Cache[*gtsmodel.Status] {
// Status provides access to the gtsmodel Status database cache.
func (c *GTSCaches) Status() *result.Cache[*gtsmodel.Status] {
return c.status
}
func (c *gtsCaches) Tombstone() *result.Cache[*gtsmodel.Tombstone] {
// StatusFave provides access to the gtsmodel StatusFave database cache.
func (c *GTSCaches) StatusFave() *result.Cache[*gtsmodel.StatusFave] {
return c.statusFave
}
// Tombstone provides access to the gtsmodel Tombstone database cache.
func (c *GTSCaches) Tombstone() *result.Cache[*gtsmodel.Tombstone] {
return c.tombstone
}
func (c *gtsCaches) User() *result.Cache[*gtsmodel.User] {
// User provides access to the gtsmodel User database cache.
func (c *GTSCaches) User() *result.Cache[*gtsmodel.User] {
return c.user
}
func (c *gtsCaches) Webfinger() *ttl.Cache[string, string] {
// Webfinger provides access to the webfinger URL cache.
func (c *GTSCaches) Webfinger() *ttl.Cache[string, string] {
return c.webfinger
}
func (c *gtsCaches) initAccount() {
func (c *GTSCaches) initAccount() {
c.account = result.New([]result.Lookup{
{Name: "ID"},
{Name: "URI"},
{Name: "URL"},
{Name: "Username.Domain"},
{Name: "PublicKeyURI"},
{Name: "InboxURI"},
{Name: "OutboxURI"},
{Name: "FollowersURI"},
{Name: "FollowingURI"},
}, func(a1 *gtsmodel.Account) *gtsmodel.Account {
a2 := new(gtsmodel.Account)
*a2 = *a1
return a2
}, config.GetCacheGTSAccountMaxSize())
c.account.SetTTL(config.GetCacheGTSAccountTTL(), true)
c.account.IgnoreErrors(ignoreErrors)
}
func (c *gtsCaches) initBlock() {
func (c *GTSCaches) initBlock() {
c.block = result.New([]result.Lookup{
{Name: "ID"},
{Name: "AccountID.TargetAccountID"},
{Name: "URI"},
{Name: "AccountID.TargetAccountID"},
}, func(b1 *gtsmodel.Block) *gtsmodel.Block {
b2 := new(gtsmodel.Block)
*b2 = *b1
return b2
}, config.GetCacheGTSBlockMaxSize())
c.block.SetTTL(config.GetCacheGTSBlockTTL(), true)
c.block.IgnoreErrors(ignoreErrors)
}
func (c *gtsCaches) initDomainBlock() {
func (c *GTSCaches) initDomainBlock() {
c.domainBlock = domain.New(
config.GetCacheGTSDomainBlockMaxSize(),
config.GetCacheGTSDomainBlockTTL(),
)
}
func (c *gtsCaches) initEmoji() {
func (c *GTSCaches) initEmoji() {
c.emoji = result.New([]result.Lookup{
{Name: "ID"},
{Name: "URI"},
@@ -270,9 +251,10 @@ func (c *gtsCaches) initEmoji() {
return e2
}, config.GetCacheGTSEmojiMaxSize())
c.emoji.SetTTL(config.GetCacheGTSEmojiTTL(), true)
c.emoji.IgnoreErrors(ignoreErrors)
}
func (c *gtsCaches) initEmojiCategory() {
func (c *GTSCaches) initEmojiCategory() {
c.emojiCategory = result.New([]result.Lookup{
{Name: "ID"},
{Name: "Name"},
@@ -282,9 +264,36 @@ func (c *gtsCaches) initEmojiCategory() {
return c2
}, config.GetCacheGTSEmojiCategoryMaxSize())
c.emojiCategory.SetTTL(config.GetCacheGTSEmojiCategoryTTL(), true)
c.emojiCategory.IgnoreErrors(ignoreErrors)
}
func (c *gtsCaches) initMedia() {
func (c *GTSCaches) initFollow() {
c.follow = result.New([]result.Lookup{
{Name: "ID"},
{Name: "URI"},
{Name: "AccountID.TargetAccountID"},
}, func(f1 *gtsmodel.Follow) *gtsmodel.Follow {
f2 := new(gtsmodel.Follow)
*f2 = *f1
return f2
}, config.GetCacheGTSFollowMaxSize())
c.follow.SetTTL(config.GetCacheGTSFollowTTL(), true)
}
func (c *GTSCaches) initFollowRequest() {
c.followRequest = result.New([]result.Lookup{
{Name: "ID"},
{Name: "URI"},
{Name: "AccountID.TargetAccountID"},
}, func(f1 *gtsmodel.FollowRequest) *gtsmodel.FollowRequest {
f2 := new(gtsmodel.FollowRequest)
*f2 = *f1
return f2
}, config.GetCacheGTSFollowRequestMaxSize())
c.followRequest.SetTTL(config.GetCacheGTSFollowRequestTTL(), true)
}
func (c *GTSCaches) initMedia() {
c.media = result.New([]result.Lookup{
{Name: "ID"},
}, func(m1 *gtsmodel.MediaAttachment) *gtsmodel.MediaAttachment {
@@ -293,9 +302,10 @@ func (c *gtsCaches) initMedia() {
return m2
}, config.GetCacheGTSMediaMaxSize())
c.media.SetTTL(config.GetCacheGTSMediaTTL(), true)
c.media.IgnoreErrors(ignoreErrors)
}
func (c *gtsCaches) initMention() {
func (c *GTSCaches) initMention() {
c.mention = result.New([]result.Lookup{
{Name: "ID"},
}, func(m1 *gtsmodel.Mention) *gtsmodel.Mention {
@@ -304,9 +314,10 @@ func (c *gtsCaches) initMention() {
return m2
}, config.GetCacheGTSMentionMaxSize())
c.mention.SetTTL(config.GetCacheGTSMentionTTL(), true)
c.mention.IgnoreErrors(ignoreErrors)
}
func (c *gtsCaches) initNotification() {
func (c *GTSCaches) initNotification() {
c.notification = result.New([]result.Lookup{
{Name: "ID"},
}, func(n1 *gtsmodel.Notification) *gtsmodel.Notification {
@@ -315,9 +326,10 @@ func (c *gtsCaches) initNotification() {
return n2
}, config.GetCacheGTSNotificationMaxSize())
c.notification.SetTTL(config.GetCacheGTSNotificationTTL(), true)
c.notification.IgnoreErrors(ignoreErrors)
}
func (c *gtsCaches) initReport() {
func (c *GTSCaches) initReport() {
c.report = result.New([]result.Lookup{
{Name: "ID"},
}, func(r1 *gtsmodel.Report) *gtsmodel.Report {
@@ -326,9 +338,10 @@ func (c *gtsCaches) initReport() {
return r2
}, config.GetCacheGTSReportMaxSize())
c.report.SetTTL(config.GetCacheGTSReportTTL(), true)
c.report.IgnoreErrors(ignoreErrors)
}
func (c *gtsCaches) initStatus() {
func (c *GTSCaches) initStatus() {
c.status = result.New([]result.Lookup{
{Name: "ID"},
{Name: "URI"},
@@ -339,10 +352,24 @@ func (c *gtsCaches) initStatus() {
return s2
}, config.GetCacheGTSStatusMaxSize())
c.status.SetTTL(config.GetCacheGTSStatusTTL(), true)
c.status.IgnoreErrors(ignoreErrors)
}
func (c *GTSCaches) initStatusFave() {
c.statusFave = result.New([]result.Lookup{
{Name: "ID"},
{Name: "AccountID.StatusID"},
}, func(f1 *gtsmodel.StatusFave) *gtsmodel.StatusFave {
f2 := new(gtsmodel.StatusFave)
*f2 = *f1
return f2
}, config.GetCacheGTSStatusFaveMaxSize())
c.status.SetTTL(config.GetCacheGTSStatusFaveTTL(), true)
c.status.IgnoreErrors(ignoreErrors)
}
// initTombstone will initialize the gtsmodel.Tombstone cache.
func (c *gtsCaches) initTombstone() {
func (c *GTSCaches) initTombstone() {
c.tombstone = result.New([]result.Lookup{
{Name: "ID"},
{Name: "URI"},
@@ -352,9 +379,10 @@ func (c *gtsCaches) initTombstone() {
return t2
}, config.GetCacheGTSTombstoneMaxSize())
c.tombstone.SetTTL(config.GetCacheGTSTombstoneTTL(), true)
c.tombstone.IgnoreErrors(ignoreErrors)
}
func (c *gtsCaches) initUser() {
func (c *GTSCaches) initUser() {
c.user = result.New([]result.Lookup{
{Name: "ID"},
{Name: "AccountID"},
@@ -367,9 +395,10 @@ func (c *gtsCaches) initUser() {
return u2
}, config.GetCacheGTSUserMaxSize())
c.user.SetTTL(config.GetCacheGTSUserTTL(), true)
c.user.IgnoreErrors(ignoreErrors)
}
func (c *gtsCaches) initWebfinger() {
func (c *GTSCaches) initWebfinger() {
c.webfinger = ttl.New[string, string](
0,
config.GetCacheGTSWebfingerMaxSize(),

View File

@@ -17,7 +17,30 @@
package cache
import "github.com/superseriousbusiness/gotosocial/internal/log"
import (
"context"
"errors"
"fmt"
"time"
"codeberg.org/gruf/go-cache/v3/result"
errorsv2 "codeberg.org/gruf/go-errors/v2"
"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.
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.
func ignoreErrors(err error) bool {
return errorsv2.Is(
SentinelError,
context.DeadlineExceeded,
context.Canceled,
)
}
// nocopy when embedded will signal linter to
// error on pass-by-value of parent struct.
@@ -27,6 +50,26 @@ func (*nocopy) Lock() {}
func (*nocopy) Unlock() {}
// tryStart will attempt to start the given cache only if sweep duration > 0 (sweeping is enabled).
func tryStart[ValueType any](cache *result.Cache[ValueType], sweep time.Duration) {
if sweep > 0 {
var z ValueType
msg := fmt.Sprintf("starting %T cache", z)
tryUntil(msg, 5, func() bool {
return cache.Start(sweep)
})
}
}
// tryStop will attempt to stop the given cache only if sweep duration > 0 (sweeping is enabled).
func tryStop[ValueType any](cache *result.Cache[ValueType], sweep time.Duration) {
if sweep > 0 {
var z ValueType
msg := fmt.Sprintf("stopping %T cache", z)
tryUntil(msg, 5, cache.Stop)
}
}
// tryUntil will attempt to call 'do' for 'count' attempts, before panicking with 'msg'.
func tryUntil(msg string, count int, do func() bool) {
for i := 0; i < count; i++ {

81
internal/cache/visibility.go vendored Normal file
View File

@@ -0,0 +1,81 @@
// 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/result"
"github.com/superseriousbusiness/gotosocial/internal/config"
)
type VisibilityCache struct {
*result.Cache[*CachedVisibility]
}
// Init will initialize the visibility cache in this collection.
// NOTE: the cache MUST NOT be in use anywhere, this is not thread-safe.
func (c *VisibilityCache) Init() {
c.Cache = result.New([]result.Lookup{
{Name: "ItemID"},
{Name: "RequesterID"},
{Name: "Type.RequesterID.ItemID"},
}, func(v1 *CachedVisibility) *CachedVisibility {
v2 := new(CachedVisibility)
*v2 = *v1
return v2
}, config.GetCacheVisibilityMaxSize())
c.Cache.SetTTL(config.GetCacheVisibilityTTL(), true)
c.Cache.IgnoreErrors(ignoreErrors)
}
// Start will attempt to start the visibility cache, or panic.
func (c *VisibilityCache) Start() {
tryStart(c.Cache, config.GetCacheVisibilitySweepFreq())
}
// Stop will attempt to stop the visibility cache, or panic.
func (c *VisibilityCache) Stop() {
tryStop(c.Cache, config.GetCacheVisibilitySweepFreq())
}
// VisibilityType represents a visibility lookup type.
// We use a byte type here to improve performance in the
// result cache when generating the key.
type VisibilityType byte
const (
// Possible cache visibility lookup types.
VisibilityTypeAccount = VisibilityType('a')
VisibilityTypeStatus = VisibilityType('s')
VisibilityTypeHome = VisibilityType('h')
VisibilityTypePublic = VisibilityType('p')
)
// CachedVisibility represents a cached visibility lookup value.
type CachedVisibility struct {
// ItemID is the ID of the item in question (status / account).
ItemID string
// RequesterID is the ID of the requesting account for this visibility lookup.
RequesterID string
// Type is the visibility lookup type.
Type VisibilityType
// Value is the actual visibility value.
Value bool
}