diff options
Diffstat (limited to 'internal/cache')
| -rw-r--r-- | internal/cache/ap.go | 29 | ||||
| -rw-r--r-- | internal/cache/cache.go | 80 | ||||
| -rw-r--r-- | internal/cache/gts.go | 313 | ||||
| -rw-r--r-- | internal/cache/util.go | 45 | ||||
| -rw-r--r-- | internal/cache/visibility.go | 81 | 
5 files changed, 373 insertions, 175 deletions
| diff --git a/internal/cache/ap.go b/internal/cache/ap.go index 204752f54..6498d7991 100644 --- a/internal/cache/ap.go +++ b/internal/cache/ap.go @@ -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() {} diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 834542a52..913d6eca7 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -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) +	})  } diff --git a/internal/cache/gts.go b/internal/cache/gts.go index 72c3211a8..392fc8449 100644 --- a/internal/cache/gts.go +++ b/internal/cache/gts.go @@ -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 >sCaches{} -} - -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()) -	}) -	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()) +// 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.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) -} - -func (c *gtsCaches) Account() *result.Cache[*gtsmodel.Account] { +// 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) +} + +// 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(), diff --git a/internal/cache/util.go b/internal/cache/util.go index 066e477e9..1ffd72876 100644 --- a/internal/cache/util.go +++ b/internal/cache/util.go @@ -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++ { diff --git a/internal/cache/visibility.go b/internal/cache/visibility.go new file mode 100644 index 000000000..8706a8015 --- /dev/null +++ b/internal/cache/visibility.go @@ -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 +} | 
