diff options
Diffstat (limited to 'internal/cache')
-rw-r--r-- | internal/cache/account.go | 157 | ||||
-rw-r--r-- | internal/cache/account_test.go | 63 | ||||
-rw-r--r-- | internal/cache/status.go | 70 | ||||
-rw-r--r-- | internal/cache/status_test.go | 66 |
4 files changed, 322 insertions, 34 deletions
diff --git a/internal/cache/account.go b/internal/cache/account.go new file mode 100644 index 000000000..bb402d60f --- /dev/null +++ b/internal/cache/account.go @@ -0,0 +1,157 @@ +package cache + +import ( + "sync" + + "github.com/ReneKroon/ttlcache" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// AccountCache is a wrapper around ttlcache.Cache to provide URL and URI lookups for gtsmodel.Account +type AccountCache struct { + cache *ttlcache.Cache // map of IDs -> cached accounts + urls map[string]string // map of account URLs -> IDs + uris map[string]string // map of account URIs -> IDs + mutex sync.Mutex +} + +// NewAccountCache returns a new instantiated AccountCache object +func NewAccountCache() *AccountCache { + c := AccountCache{ + cache: ttlcache.NewCache(), + urls: make(map[string]string, 100), + uris: make(map[string]string, 100), + mutex: sync.Mutex{}, + } + + // Set callback to purge lookup maps on expiration + c.cache.SetExpirationCallback(func(key string, value interface{}) { + account := value.(*gtsmodel.Account) + + c.mutex.Lock() + delete(c.urls, account.URL) + delete(c.uris, account.URI) + c.mutex.Unlock() + }) + + return &c +} + +// GetByID attempts to fetch a account from the cache by its ID, you will receive a copy for thread-safety +func (c *AccountCache) GetByID(id string) (*gtsmodel.Account, bool) { + c.mutex.Lock() + account, ok := c.getByID(id) + c.mutex.Unlock() + return account, ok +} + +// GetByURL attempts to fetch a account from the cache by its URL, you will receive a copy for thread-safety +func (c *AccountCache) GetByURL(url string) (*gtsmodel.Account, bool) { + // Perform safe ID lookup + c.mutex.Lock() + id, ok := c.urls[url] + + // Not found, unlock early + if !ok { + c.mutex.Unlock() + return nil, false + } + + // Attempt account lookup + account, ok := c.getByID(id) + c.mutex.Unlock() + return account, ok +} + +// GetByURI attempts to fetch a account from the cache by its URI, you will receive a copy for thread-safety +func (c *AccountCache) GetByURI(uri string) (*gtsmodel.Account, bool) { + // Perform safe ID lookup + c.mutex.Lock() + id, ok := c.uris[uri] + + // Not found, unlock early + if !ok { + c.mutex.Unlock() + return nil, false + } + + // Attempt account lookup + account, ok := c.getByID(id) + c.mutex.Unlock() + return account, ok +} + +// getByID performs an unsafe (no mutex locks) lookup of account by ID, returning a copy of account in cache +func (c *AccountCache) getByID(id string) (*gtsmodel.Account, bool) { + v, ok := c.cache.Get(id) + if !ok { + return nil, false + } + return copyAccount(v.(*gtsmodel.Account)), true +} + +// Put places a account in the cache, ensuring that the object place is a copy for thread-safety +func (c *AccountCache) Put(account *gtsmodel.Account) { + if account == nil || account.ID == "" { + panic("invalid account") + } + + c.mutex.Lock() + c.cache.Set(account.ID, copyAccount(account)) + if account.URL != "" { + c.urls[account.URL] = account.ID + } + if account.URI != "" { + c.uris[account.URI] = account.ID + } + c.mutex.Unlock() +} + +// copyAccount performs a surface-level copy of account, only keeping attached IDs intact, not the objects. +// due to all the data being copied being 99% primitive types or strings (which are immutable and passed by ptr) +// this should be a relatively cheap process +func copyAccount(account *gtsmodel.Account) *gtsmodel.Account { + return >smodel.Account{ + ID: account.ID, + Username: account.Username, + Domain: account.Domain, + AvatarMediaAttachmentID: account.AvatarMediaAttachmentID, + AvatarMediaAttachment: nil, + AvatarRemoteURL: account.AvatarRemoteURL, + HeaderMediaAttachmentID: account.HeaderMediaAttachmentID, + HeaderMediaAttachment: nil, + HeaderRemoteURL: account.HeaderRemoteURL, + DisplayName: account.DisplayName, + Fields: account.Fields, + Note: account.Note, + Memorial: account.Memorial, + MovedToAccountID: account.MovedToAccountID, + CreatedAt: account.CreatedAt, + UpdatedAt: account.UpdatedAt, + Bot: account.Bot, + Reason: account.Reason, + Locked: account.Locked, + Discoverable: account.Discoverable, + Privacy: account.Privacy, + Sensitive: account.Sensitive, + Language: account.Language, + URI: account.URI, + URL: account.URL, + LastWebfingeredAt: account.LastWebfingeredAt, + InboxURI: account.InboxURI, + OutboxURI: account.OutboxURI, + FollowingURI: account.FollowingURI, + FollowersURI: account.FollowersURI, + FeaturedCollectionURI: account.FeaturedCollectionURI, + ActorType: account.ActorType, + AlsoKnownAs: account.AlsoKnownAs, + PrivateKey: account.PrivateKey, + PublicKey: account.PublicKey, + PublicKeyURI: account.PublicKeyURI, + SensitizedAt: account.SensitizedAt, + SilencedAt: account.SilencedAt, + SuspendedAt: account.SuspendedAt, + HideCollections: account.HideCollections, + SuspensionOrigin: account.SuspensionOrigin, + } +} diff --git a/internal/cache/account_test.go b/internal/cache/account_test.go new file mode 100644 index 000000000..f84ad2261 --- /dev/null +++ b/internal/cache/account_test.go @@ -0,0 +1,63 @@ +package cache_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/cache" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type AccountCacheTestSuite struct { + suite.Suite + data map[string]*gtsmodel.Account + cache *cache.AccountCache +} + +func (suite *AccountCacheTestSuite) SetupSuite() { + suite.data = testrig.NewTestAccounts() +} + +func (suite *AccountCacheTestSuite) SetupTest() { + suite.cache = cache.NewAccountCache() +} + +func (suite *AccountCacheTestSuite) TearDownTest() { + suite.data = nil + suite.cache = nil +} + +func (suite *AccountCacheTestSuite) TestAccountCache() { + for _, account := range suite.data { + // Place in the cache + suite.cache.Put(account) + } + + for _, account := range suite.data { + var ok bool + var check *gtsmodel.Account + + // Check we can retrieve + check, ok = suite.cache.GetByID(account.ID) + if !ok && !accountIs(account, check) { + suite.Fail("Failed to fetch expected account with ID: %s", account.ID) + } + check, ok = suite.cache.GetByURI(account.URI) + if account.URI != "" && !ok && !accountIs(account, check) { + suite.Fail("Failed to fetch expected account with URI: %s", account.URI) + } + check, ok = suite.cache.GetByURL(account.URL) + if account.URL != "" && !ok && !accountIs(account, check) { + suite.Fail("Failed to fetch expected account with URL: %s", account.URL) + } + } +} + +func TestAccountCache(t *testing.T) { + suite.Run(t, &AccountCacheTestSuite{}) +} + +func accountIs(account1, account2 *gtsmodel.Account) bool { + return account1.ID == account2.ID && account1.URI == account2.URI && account1.URL == account2.URL +} diff --git a/internal/cache/status.go b/internal/cache/status.go index 895a5692c..028abc8f7 100644 --- a/internal/cache/status.go +++ b/internal/cache/status.go @@ -37,7 +37,7 @@ func NewStatusCache() *StatusCache { return &c } -// GetByID attempts to fetch a status from the cache by its ID +// GetByID attempts to fetch a status from the cache by its ID, you will receive a copy for thread-safety func (c *StatusCache) GetByID(id string) (*gtsmodel.Status, bool) { c.mutex.Lock() status, ok := c.getByID(id) @@ -45,7 +45,7 @@ func (c *StatusCache) GetByID(id string) (*gtsmodel.Status, bool) { return status, ok } -// GetByURL attempts to fetch a status from the cache by its URL +// GetByURL attempts to fetch a status from the cache by its URL, you will receive a copy for thread-safety func (c *StatusCache) GetByURL(url string) (*gtsmodel.Status, bool) { // Perform safe ID lookup c.mutex.Lock() @@ -63,7 +63,7 @@ func (c *StatusCache) GetByURL(url string) (*gtsmodel.Status, bool) { return status, ok } -// GetByURI attempts to fetch a status from the cache by its URI +// GetByURI attempts to fetch a status from the cache by its URI, you will receive a copy for thread-safety func (c *StatusCache) GetByURI(uri string) (*gtsmodel.Status, bool) { // Perform safe ID lookup c.mutex.Lock() @@ -81,26 +81,72 @@ func (c *StatusCache) GetByURI(uri string) (*gtsmodel.Status, bool) { return status, ok } -// getByID performs an unsafe (no mutex locks) lookup of status by ID +// getByID performs an unsafe (no mutex locks) lookup of status by ID, returning a copy of status in cache func (c *StatusCache) getByID(id string) (*gtsmodel.Status, bool) { v, ok := c.cache.Get(id) if !ok { return nil, false } - return v.(*gtsmodel.Status), true + return copyStatus(v.(*gtsmodel.Status)), true } -// Put places a status in the cache +// Put places a status in the cache, ensuring that the object place is a copy for thread-safety func (c *StatusCache) Put(status *gtsmodel.Status) { - if status == nil || status.ID == "" || - status.URL == "" || - status.URI == "" { + if status == nil || status.ID == "" { panic("invalid status") } c.mutex.Lock() - c.cache.Set(status.ID, status) - c.urls[status.URL] = status.ID - c.uris[status.URI] = status.ID + c.cache.Set(status.ID, copyStatus(status)) + if status.URL != "" { + c.urls[status.URL] = status.ID + } + if status.URI != "" { + c.uris[status.URI] = status.ID + } c.mutex.Unlock() } + +// copyStatus performs a surface-level copy of status, only keeping attached IDs intact, not the objects. +// due to all the data being copied being 99% primitive types or strings (which are immutable and passed by ptr) +// this should be a relatively cheap process +func copyStatus(status *gtsmodel.Status) *gtsmodel.Status { + return >smodel.Status{ + ID: status.ID, + URI: status.URI, + URL: status.URL, + Content: status.Content, + AttachmentIDs: status.AttachmentIDs, + Attachments: nil, + TagIDs: status.TagIDs, + Tags: nil, + MentionIDs: status.MentionIDs, + Mentions: nil, + EmojiIDs: status.EmojiIDs, + Emojis: nil, + CreatedAt: status.CreatedAt, + UpdatedAt: status.UpdatedAt, + Local: status.Local, + AccountID: status.AccountID, + Account: nil, + AccountURI: status.AccountURI, + InReplyToID: status.InReplyToID, + InReplyTo: nil, + InReplyToURI: status.InReplyToURI, + InReplyToAccountID: status.InReplyToAccountID, + InReplyToAccount: nil, + BoostOfID: status.BoostOfID, + BoostOf: nil, + BoostOfAccountID: status.BoostOfAccountID, + BoostOfAccount: nil, + ContentWarning: status.ContentWarning, + Visibility: status.Visibility, + Sensitive: status.Sensitive, + Language: status.Language, + CreatedWithApplicationID: status.CreatedWithApplicationID, + VisibilityAdvanced: status.VisibilityAdvanced, + ActivityStreamsType: status.ActivityStreamsType, + Text: status.Text, + Pinned: status.Pinned, + } +} diff --git a/internal/cache/status_test.go b/internal/cache/status_test.go index 10dee5bca..222961025 100644 --- a/internal/cache/status_test.go +++ b/internal/cache/status_test.go @@ -3,39 +3,61 @@ package cache_test import ( "testing" + "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/cache" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/testrig" ) -func TestStatusCache(t *testing.T) { - cache := cache.NewStatusCache() +type StatusCacheTestSuite struct { + suite.Suite + data map[string]*gtsmodel.Status + cache *cache.StatusCache +} - // Attempt to place a status - status := gtsmodel.Status{ - ID: "id", - URI: "uri", - URL: "url", - } - cache.Put(&status) +func (suite *StatusCacheTestSuite) SetupSuite() { + suite.data = testrig.NewTestStatuses() +} - var ok bool - var check *gtsmodel.Status +func (suite *StatusCacheTestSuite) SetupTest() { + suite.cache = cache.NewStatusCache() +} - // Check we can retrieve - check, ok = cache.GetByID(status.ID) - if !ok || !statusIs(&status, check) { - t.Fatal("Could not find expected status") - } - check, ok = cache.GetByURI(status.URI) - if !ok || !statusIs(&status, check) { - t.Fatal("Could not find expected status") +func (suite *StatusCacheTestSuite) TearDownTest() { + suite.data = nil + suite.cache = nil +} + +func (suite *StatusCacheTestSuite) TestStatusCache() { + for _, status := range suite.data { + // Place in the cache + suite.cache.Put(status) } - check, ok = cache.GetByURL(status.URL) - if !ok || !statusIs(&status, check) { - t.Fatal("Could not find expected status") + + for _, status := range suite.data { + var ok bool + var check *gtsmodel.Status + + // Check we can retrieve + check, ok = suite.cache.GetByID(status.ID) + if !ok && !statusIs(status, check) { + suite.Fail("Failed to fetch expected account with ID: %s", status.ID) + } + check, ok = suite.cache.GetByURI(status.URI) + if status.URI != "" && !ok && !statusIs(status, check) { + suite.Fail("Failed to fetch expected account with URI: %s", status.URI) + } + check, ok = suite.cache.GetByURL(status.URL) + if status.URL != "" && !ok && !statusIs(status, check) { + suite.Fail("Failed to fetch expected account with URL: %s", status.URL) + } } } +func TestStatusCache(t *testing.T) { + suite.Run(t, &StatusCacheTestSuite{}) +} + func statusIs(status1, status2 *gtsmodel.Status) bool { return status1.ID == status2.ID && status1.URI == status2.URI && status1.URL == status2.URL } |