summaryrefslogtreecommitdiff
path: root/internal/cache
diff options
context:
space:
mode:
Diffstat (limited to 'internal/cache')
-rw-r--r--internal/cache/account.go157
-rw-r--r--internal/cache/account_test.go63
-rw-r--r--internal/cache/status.go70
-rw-r--r--internal/cache/status_test.go66
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 &gtsmodel.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 &gtsmodel.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
}