diff --git a/api/context.go b/api/context.go index ab806dde6f8..5a744b887e3 100644 --- a/api/context.go +++ b/api/context.go @@ -582,6 +582,7 @@ func InvalidateAllCaches() { store.ClearUserCaches() store.ClearPostCaches() store.ClearWebhookCaches() + store.ClearFileCaches() } func (c *Context) CheckTeamId() { diff --git a/api/file.go b/api/file.go index 0774a78b828..ce4c10d8584 100644 --- a/api/file.go +++ b/api/file.go @@ -630,7 +630,7 @@ func migrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo { return []*model.FileInfo{} } else if newPost := result.Data.(*model.PostList).Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) { // Another thread has already created FileInfos for this post, so just return those - if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil { + if result := <-Srv.Store.FileInfo().GetForPost(post.Id, false); result.Err != nil { l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_file_infos_again.app_error"), post.Id, result.Err) return []*model.FileInfo{} } else { diff --git a/api/post.go b/api/post.go index eb3dcad68d8..2b62666ada7 100644 --- a/api/post.go +++ b/api/post.go @@ -616,7 +616,7 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * senderUsername = c.T("system.message.name") } else { pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true) - fchan = Srv.Store.FileInfo().GetForPost(post.Id) + fchan = Srv.Store.FileInfo().GetForPost(post.Id, true) var profileMap map[string]*model.User if result := <-pchan; result.Err != nil { @@ -1004,7 +1004,7 @@ func getMessageForNotification(post *model.Post, translateFunc i18n.TranslateFun // extract the filenames from their paths and determine what type of files are attached var infos []*model.FileInfo - if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil { + if result := <-Srv.Store.FileInfo().GetForPost(post.Id, true); result.Err != nil { l4g.Warn(utils.T("api.post.get_message_for_notification.get_files.error"), post.Id, result.Err) } else { infos = result.Data.([]*model.FileInfo) @@ -1691,7 +1691,7 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) { } pchan := Srv.Store.Post().Get(postId) - fchan := Srv.Store.FileInfo().GetForPost(postId) + fchan := Srv.Store.FileInfo().GetForPost(postId, true) if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { return @@ -1716,6 +1716,7 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) { } if len(post.Filenames) > 0 { + Srv.Store.FileInfo().InvalidateFileInfosForPostCache(postId) // The post has Filenames that need to be replaced with FileInfos infos = migrateFilenamesToFileInfos(post) } diff --git a/api/post_test.go b/api/post_test.go index 8595b323bfc..89fce3597cf 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -130,7 +130,7 @@ func TestCreatePost(t *testing.T) { } else if rpost9 := resp.Data.(*model.Post); len(rpost9.FileIds) != 3 { t.Fatal("post should have 3 files") } else { - infos := store.Must(Srv.Store.FileInfo().GetForPost(rpost9.Id)).([]*model.FileInfo) + infos := store.Must(Srv.Store.FileInfo().GetForPost(rpost9.Id, true)).([]*model.FileInfo) if len(infos) != 3 { t.Fatal("should've attached all 3 files to post") @@ -1267,6 +1267,7 @@ func TestGetMessageForNotification(t *testing.T) { post.FileIds = model.StringArray{testPng.Id, testJpg1.Id} store.Must(Srv.Store.FileInfo().AttachToPost(testJpg1.Id, post.Id)) + Srv.Store.FileInfo().InvalidateFileInfosForPostCache(post.Id) if message := getMessageForNotification(post, translateFunc); message != "2 images sent: test1.png, test2.jpg" && message != "2 images sent: test2.jpg, test1.png" { t.Fatal("should've returned number of images:", message) } @@ -1280,6 +1281,7 @@ func TestGetMessageForNotification(t *testing.T) { store.Must(Srv.Store.FileInfo().AttachToPost(testJpg2.Id, post.Id)) post.FileIds = model.StringArray{testFile.Id, testJpg2.Id} + Srv.Store.FileInfo().InvalidateFileInfosForPostCache(post.Id) if message := getMessageForNotification(post, translateFunc); message != "2 files sent: test1.go, test3.jpg" && message != "2 files sent: test3.jpg, test1.go" { t.Fatal("should've returned number of mixed files:", message) } diff --git a/store/sql_file_info_store.go b/store/sql_file_info_store.go index 762eac5d4ee..4da707cfb41 100644 --- a/store/sql_file_info_store.go +++ b/store/sql_file_info_store.go @@ -3,13 +3,26 @@ package store import ( + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) type SqlFileInfoStore struct { *SqlStore } +const ( + FILE_INFO_CACHE_SIZE = 25000 + FILE_INFO_CACHE_SEC = 900 // 15 minutes +) + +var fileInfoCache *utils.Cache = utils.NewLru(FILE_INFO_CACHE_SIZE) + +func ClearFileCaches() { + fileInfoCache.Purge() +} + func NewSqlFileInfoStore(sqlStore *SqlStore) FileInfoStore { s := &SqlFileInfoStore{sqlStore} @@ -119,11 +132,37 @@ func (fs SqlFileInfoStore) GetByPath(path string) StoreChannel { return storeChannel } -func (fs SqlFileInfoStore) GetForPost(postId string) StoreChannel { +func (s SqlFileInfoStore) InvalidateFileInfosForPostCache(postId string) { + fileInfoCache.Remove(postId) +} + +func (fs SqlFileInfoStore) GetForPost(postId string, allowFromCache bool) StoreChannel { storeChannel := make(StoreChannel, 1) go func() { result := StoreResult{} + metrics := einterfaces.GetMetricsInterface() + + if allowFromCache { + if cacheItem, ok := fileInfoCache.Get(postId); ok { + if metrics != nil { + metrics.IncrementMemCacheHitCounter("File Info Cache") + } + + result.Data = cacheItem.([]*model.FileInfo) + storeChannel <- result + close(storeChannel) + return + } else { + if metrics != nil { + metrics.IncrementMemCacheMissCounter("File Info Cache") + } + } + } else { + if metrics != nil { + metrics.IncrementMemCacheMissCounter("File Info Cache") + } + } var infos []*model.FileInfo @@ -140,6 +179,7 @@ func (fs SqlFileInfoStore) GetForPost(postId string) StoreChannel { result.Err = model.NewLocAppError("SqlFileInfoStore.GetForPost", "store.sql_file_info.get_for_post.app_error", nil, "post_id="+postId+", "+err.Error()) } else { + fileInfoCache.AddWithExpiresInSecs(postId, infos, FILE_INFO_CACHE_SEC) result.Data = infos } diff --git a/store/sql_file_info_store_test.go b/store/sql_file_info_store_test.go index edb9dbd5475..672e15ef2d1 100644 --- a/store/sql_file_info_store_test.go +++ b/store/sql_file_info_store_test.go @@ -114,7 +114,13 @@ func TestFileInfoGetForPost(t *testing.T) { infos[i] = Must(store.FileInfo().Save(info)).(*model.FileInfo) } - if result := <-store.FileInfo().GetForPost(postId); result.Err != nil { + if result := <-store.FileInfo().GetForPost(postId, false); result.Err != nil { + t.Fatal(result.Err) + } else if returned := result.Data.([]*model.FileInfo); len(returned) != 2 { + t.Fatal("should've returned exactly 2 file infos") + } + + if result := <-store.FileInfo().GetForPost(postId, true); result.Err != nil { t.Fatal(result.Err) } else if returned := result.Data.([]*model.FileInfo); len(returned) != 2 { t.Fatal("should've returned exactly 2 file infos") @@ -157,7 +163,7 @@ func TestFileInfoAttachToPost(t *testing.T) { info2 = Must(store.FileInfo().Get(info2.Id)).(*model.FileInfo) } - if result := <-store.FileInfo().GetForPost(postId); result.Err != nil { + if result := <-store.FileInfo().GetForPost(postId, false); result.Err != nil { t.Fatal(result.Err) } else if infos := result.Data.([]*model.FileInfo); len(infos) != 2 { t.Fatal("should've returned exactly 2 file infos") @@ -202,7 +208,7 @@ func TestFileInfoDeleteForPost(t *testing.T) { t.Fatal(result.Err) } - if infos := Must(store.FileInfo().GetForPost(postId)).([]*model.FileInfo); len(infos) != 0 { + if infos := Must(store.FileInfo().GetForPost(postId, false)).([]*model.FileInfo); len(infos) != 0 { t.Fatal("shouldn't have returned any file infos") } } diff --git a/store/store.go b/store/store.go index dcbc7d501e3..65ca399aa28 100644 --- a/store/store.go +++ b/store/store.go @@ -317,7 +317,8 @@ type FileInfoStore interface { Save(info *model.FileInfo) StoreChannel Get(id string) StoreChannel GetByPath(path string) StoreChannel - GetForPost(postId string) StoreChannel + GetForPost(postId string, allowFromCache bool) StoreChannel + InvalidateFileInfosForPostCache(postId string) AttachToPost(fileId string, postId string) StoreChannel DeleteForPost(postId string) StoreChannel }