Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scheduled post #848

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ require (
github.com/gliderlabs/ssh v0.1.1
github.com/grafana/alloy/syntax v0.1.0
github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a
github.com/mattermost/mattermost/server/public v0.1.8-0.20241015185928-63c97f5a6d8f
github.com/mattermost/mattermost/server/public v0.1.9
github.com/mattermost/mattermost/server/v8 v8.0.0-20241015185928-63c97f5a6d8f
github.com/opensearch-project/opensearch-go/v4 v4.1.0
github.com/pelletier/go-toml/v2 v2.2.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy5
github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc=
github.com/mattermost/mattermost/server/public v0.1.8-0.20241015185928-63c97f5a6d8f h1:NUAf56HZHFLayAyCqHxeVLmxJUN9xw3Qxc9m3ghy+Xw=
github.com/mattermost/mattermost/server/public v0.1.8-0.20241015185928-63c97f5a6d8f/go.mod h1:SkTKbMul91Rq0v2dIxe8mqzUOY+3KwlwwLmAlxDfGCk=
github.com/mattermost/mattermost/server/public v0.1.9 h1:l/OKPRVuFeqL0yqRVC/JpveG5sLNKcT9llxqMkO9e+s=
github.com/mattermost/mattermost/server/public v0.1.9/go.mod h1:SkTKbMul91Rq0v2dIxe8mqzUOY+3KwlwwLmAlxDfGCk=
github.com/mattermost/mattermost/server/v8 v8.0.0-20241015185928-63c97f5a6d8f h1:h4T89Qkb3kddGnRN7xQAuB+fDaL3OgUgc5YPmssmzj8=
github.com/mattermost/mattermost/server/v8 v8.0.0-20241015185928-63c97f5a6d8f/go.mod h1:zwMZYGK4/xDy/kyepVBqXhM58YMTCRlanv/uKrZzJdw=
github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6Cw=
Expand Down
9 changes: 9 additions & 0 deletions loadtest/control/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,15 @@ func DraftsEnabled(u user.User) (bool, UserActionResponse) {

func ChannelBookmarkEnabled(u user.User) (bool, UserActionResponse) {
allow := u.Store().FeatureFlags()["ChannelBookmarks"]
return allow, UserActionResponse{}
}

func ScheduledPostsEnabled(u user.User) (bool, UserActionResponse) {
allow, err := strconv.ParseBool(u.Store().ClientConfig()["ScheduledPosts"])
if err != nil {
fmt.Println("Error parsing ScheduledPosts config", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary log?

return false, UserActionResponse{Err: NewUserError(err)}
}

return allow, UserActionResponse{}
}
Expand Down
4 changes: 4 additions & 0 deletions loadtest/control/simulcontroller/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ func loadTeam(u user.User, team *model.Team, gqlEnabled bool) control.UserAction
return control.UserActionResponse{Err: control.NewUserError(err)}
}

if err := u.GetTeamScheduledPosts(team.Id); err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

return control.UserActionResponse{Info: fmt.Sprintf("loaded team %s", team.Id)}
}

Expand Down
28 changes: 28 additions & 0 deletions loadtest/control/simulcontroller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import (
"github.com/mattermost/mattermost/server/public/shared/mlog"
)

const (
probabilityAttachFileToPost = 0.02
)

func getActionList(c *SimulController) []userAction {
actions := []userAction{
{
Expand Down Expand Up @@ -288,6 +292,30 @@ func getActionList(c *SimulController) []userAction {
frequency: 0.0001, // https://mattermost.atlassian.net/browse/MM-61131
minServerVersion: semver.MustParse("10.0.0"),
},
{
name: "CreateScheduledPost",
run: c.createScheduledPost,
frequency: 0.2,
minServerVersion: semver.MustParse("10.3.0"),
},
{
name: "UpdateScheduledPost",
run: c.updateScheduledPost,
frequency: 0.1,
minServerVersion: semver.MustParse("10.3.0"),
},
{
name: "DeleteScheduledPost",
run: c.deleteScheduledPost,
frequency: 0.1,
minServerVersion: semver.MustParse("10.3.0"),
},
{
name: "SendScheduledPost",
run: c.sendScheduledPostNow,
frequency: 0.1,
minServerVersion: semver.MustParse("10.3.0"),
},
// All actions are required to contain a valid minServerVersion:
// - If the action is present in server versions equal or older than
// control.MinSupportedVersion, use control.MinSupportedVersion.
Expand Down
2 changes: 1 addition & 1 deletion loadtest/control/simulcontroller/drafts.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (c *SimulController) upsertDraft(u user.User) control.UserActionResponse {
}

// 2% of the times post will have files attached.
if rand.Float64() < 0.02 {
if rand.Float64() < probabilityAttachFileToPost {
if err := control.AttachFilesToDraft(u, draft); err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}
Expand Down
155 changes: 155 additions & 0 deletions loadtest/control/simulcontroller/scheduled_posts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package simulcontroller

import (
"fmt"
"github.com/mattermost/mattermost-load-test-ng/loadtest"
"github.com/mattermost/mattermost-load-test-ng/loadtest/control"
"github.com/mattermost/mattermost-load-test-ng/loadtest/user"
"github.com/mattermost/mattermost/server/public/model"
"math/rand"
"time"
)

func (c *SimulController) createScheduledPost(u user.User) control.UserActionResponse {
if ok, resp := control.ScheduledPostsEnabled(u); resp.Err != nil {
return resp
} else if !ok {
return control.UserActionResponse{Info: "scheduled posts not enabled"}
}

channel, err := u.Store().CurrentChannel()
if err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

var rootId = ""
if rand.Float64() < 0.25 {
post, err := u.Store().RandomPostForChannel(channel.Id)
if err == nil {
Comment on lines +27 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should handle the error case.

if post.RootId != "" {
rootId = post.RootId
} else {
rootId = post.Id
}
}
}

if err := sendTypingEventIfEnabled(u, channel.Id); err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

message, err := createMessage(u, channel, false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we can schedule replies as well. A case to consider adding.

if err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
Message: message,
ChannelId: channel.Id,
RootId: rootId,
CreateAt: model.GetMillis(),
},
ScheduledAt: loadtest.RandomFutureTime(time.Hour*24*2, time.Hour*24*10),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd really welcome named constants to make it straightforward to read, e.g. TwoDays, TenDays :)

}

if rand.Float64() < probabilityAttachFileToPost {
if err := control.AttachFilesToDraft(u, &scheduledPost.Draft); err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}
}

if err := u.CreateScheduledPost(channel.TeamId, scheduledPost); err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

return control.UserActionResponse{Info: fmt.Sprintf("scheduled post created in channel with id %s", channel.Id)}
}

func (c *SimulController) updateScheduledPost(u user.User) control.UserActionResponse {
if ok, resp := control.ScheduledPostsEnabled(u); resp.Err != nil {
return resp
} else if !ok {
return control.UserActionResponse{Info: "scheduled posts not enabled"}
}

scheduledPost, err := u.Store().GetRandomScheduledPost()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we ensuring GetRandomScheduledPost() always returns non-deleted posts, if any?

if err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}
if scheduledPost == nil {
return control.UserActionResponse{Info: "no scheduled posts found"}
}

channel, err := u.Store().CurrentChannel()
if err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

message, err := createMessage(u, channel, false)
if err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

scheduledPost.Message = message
scheduledPost.ScheduledAt = loadtest.RandomFutureTime(time.Hour*24*2, time.Hour*24*10)

if err := u.UpdateScheduledPost(channel.TeamId, scheduledPost); err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

return control.UserActionResponse{Info: fmt.Sprintf("scheduled post updated in channel with id %s", channel.Id)}
}

func (c *SimulController) deleteScheduledPost(u user.User) control.UserActionResponse {
if ok, resp := control.ScheduledPostsEnabled(u); resp.Err != nil {
return resp
} else if !ok {
return control.UserActionResponse{Info: "scheduled posts not enabled"}
}

scheduledPost, err := u.Store().GetRandomScheduledPost()
if err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}
if scheduledPost == nil {
return control.UserActionResponse{Info: "no scheduled posts found"}
}

if err := u.DeleteScheduledPost(scheduledPost); err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

return control.UserActionResponse{Info: fmt.Sprintf("scheduled post with id %s deleted", scheduledPost.Id)}
}

func (c *SimulController) sendScheduledPostNow(u user.User) control.UserActionResponse {
if ok, resp := control.ScheduledPostsEnabled(u); resp.Err != nil {
return resp
} else if !ok {
return control.UserActionResponse{Info: "scheduled posts not enabled"}
}

scheduledPost, err := u.Store().GetRandomScheduledPost()
if err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}
if scheduledPost == nil {
return control.UserActionResponse{Info: "no scheduled posts found"}
}

post, err := scheduledPost.ToPost()
if err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

if _, err := u.CreatePost(post); err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

if err := u.DeleteScheduledPost(scheduledPost); err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

return control.UserActionResponse{Info: fmt.Sprintf("scheduled post with id %s manually sent now", scheduledPost.Id)}
}
40 changes: 38 additions & 2 deletions loadtest/store/memstore/random.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ package memstore

import (
"errors"
"math/rand"

"github.com/mattermost/mattermost-load-test-ng/loadtest/store"
"github.com/mattermost/mattermost/server/public/model"
"math/rand"
)

var (
Expand Down Expand Up @@ -448,3 +447,40 @@ func (s *MemStore) RandomDraftForTeam(teamId string) (string, error) {

return draftIDs[rand.Intn(len(draftIDs))], nil
}

func (s *MemStore) GetRandomScheduledPost() (*model.ScheduledPost, error) {
s.lock.RLock()
defer s.lock.RUnlock()

// Check if scheduledPosts is empty
if len(s.scheduledPosts) == 0 {
return nil, errors.New("no scheduled posts available")
}

var keys []string
for key, innerMap := range s.scheduledPosts {
if len(innerMap) > 0 {
keys = append(keys, key)
}
}

if len(keys) == 0 {
return nil, errors.New("no scheduled posts available")
}

selectedInnerMap := s.scheduledPosts[keys[rand.Intn(len(keys))]]

// Pick a random index for the inner map
randomInnerIndex := rand.Intn(len(selectedInnerMap))
var selectedPost *model.ScheduledPost
innerIndex := 0
for _, post := range selectedInnerMap {
if innerIndex == randomInnerIndex {
selectedPost = post[rand.Intn(len(post))]
break
}
innerIndex++
}

return selectedPost, nil
}
71 changes: 71 additions & 0 deletions loadtest/store/memstore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type MemStore struct {
featureFlags map[string]bool
report *model.PerformanceReport
channelBookmarks map[string]*model.ChannelBookmarkWithFileInfo
scheduledPosts map[string]map[string][]*model.ScheduledPost // map of team ID -> channel/thread ID -> list of scheduled posts
}

// New returns a new instance of MemStore with the given config.
Expand Down Expand Up @@ -130,6 +131,8 @@ func (s *MemStore) Clear() {
s.drafts = map[string]map[string]*model.Draft{}
clear(s.channelBookmarks)
s.channelBookmarks = map[string]*model.ChannelBookmarkWithFileInfo{}
clear(s.scheduledPosts)
s.scheduledPosts = map[string]map[string][]*model.ScheduledPost{}
}

func (s *MemStore) setupQueues(config *Config) error {
Expand Down Expand Up @@ -1356,6 +1359,74 @@ func (s *MemStore) DeleteChannelBookmark(bookmarkId string) error {
}

delete(s.channelBookmarks, bookmarkId)
return nil
}

func (s *MemStore) SetScheduledPost(teamId string, scheduledPost *model.ScheduledPost) error {
s.lock.Lock()
defer s.lock.Unlock()

if scheduledPost == nil {
return errors.New("memstore: scheduled post should not be nil")
}

if s.scheduledPosts == nil {
s.scheduledPosts = map[string]map[string][]*model.ScheduledPost{}
}

if s.scheduledPosts[teamId] == nil {
s.scheduledPosts[teamId] = map[string][]*model.ScheduledPost{}
}

channelOrThreadId := scheduledPost.ChannelId
if scheduledPost.RootId != "" {
channelOrThreadId = scheduledPost.RootId
}

s.scheduledPosts[teamId][channelOrThreadId] = append(s.scheduledPosts[teamId][channelOrThreadId], scheduledPost)
return nil
}

func (s *MemStore) DeleteScheduledPost(scheduledPost *model.ScheduledPost) {
s.lock.Lock()
defer s.lock.Unlock()

for teamId := range s.scheduledPosts {
channelOrThreadId := scheduledPost.ChannelId
if scheduledPost.RootId != "" {
channelOrThreadId = scheduledPost.RootId
}

// find index of scheduledPost in s.scheduledPosts[teamId][channelOrThreadId] and if found, delete it
for i, sp := range s.scheduledPosts[teamId][channelOrThreadId] {
if sp.Id == scheduledPost.Id {
s.scheduledPosts[teamId][channelOrThreadId] = append(s.scheduledPosts[teamId][channelOrThreadId][:i], s.scheduledPosts[teamId][channelOrThreadId][i+1:]...)
break
}
}
}
}

func (s *MemStore) UpdateScheduledPost(teamId string, scheduledPost *model.ScheduledPost) {
s.lock.Lock()
defer s.lock.Unlock()

channelOrThreadId := scheduledPost.ChannelId
if scheduledPost.RootId != "" {
channelOrThreadId = scheduledPost.RootId
}

if _, ok := s.scheduledPosts[teamId]; !ok {
s.scheduledPosts[teamId] = map[string][]*model.ScheduledPost{
channelOrThreadId: {scheduledPost},
}
return
}

for i := range s.scheduledPosts[teamId][channelOrThreadId] {
if s.scheduledPosts[teamId][channelOrThreadId][i].Id == scheduledPost.Id {
s.scheduledPosts[teamId][channelOrThreadId][i] = scheduledPost
break
}
}
}
Loading
Loading