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 24 commits into
base: master
Choose a base branch
from
Open

Scheduled post #848

wants to merge 24 commits into from

Conversation

harshilsharma63
Copy link
Member

Summary

Adding scheduled posts support to load test tool.

Ticket Link

Fixes https://mattermost.atlassian.net/browse/MM-61486

@harshilsharma63 harshilsharma63 marked this pull request as ready for review November 14, 2024 07:01
Copy link
Member

@agnivade agnivade left a comment

Choose a reason for hiding this comment

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

It looks like some debug fmt.Println statements are still left over from debugging. Let's clean them up.

Copy link
Member

@agarciamontoro agarciamontoro left a comment

Choose a reason for hiding this comment

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

Thank you so much for all your work on this, great job! I left some comments below, and I also agree with Agniva: we should remove all the fmt.Println lines.

@harshilsharma63
Copy link
Member Author

@agnivade @agarciamontoro can you please re-review this? I've made the fixes.

Copy link
Member

@agnivade agnivade left a comment

Choose a reason for hiding this comment

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

I will leave the store logic to Alejandro.

Copy link
Member

@agnivade agnivade left a comment

Choose a reason for hiding this comment

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

Looking good!

Copy link
Member

@agarciamontoro agarciamontoro left a comment

Choose a reason for hiding this comment

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

Thanks, @harshilsharma63, and sorry for the delay in the review! Some more comments below.

Comment on lines 56 to 57
ue.Store().DeleteScheduledPost(scheduledPostId)
return nil
Copy link
Member

Choose a reason for hiding this comment

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

Right now DeleteScheduledPost doesn't return an error, but maybe it should (I left a comment for that). If we decide that's a good decision, then we need to return its error.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think its of any use returning error from delete method, it doesn't matter

Copy link
Member

@agarciamontoro agarciamontoro left a comment

Choose a reason for hiding this comment

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

Thanks, @harshilsharma63! I've left a comment in the RandomFutureTime function, and I have an additional ask: I do think we should error out in MemStore's DeleteScheduledPost and UpdateScheduledPost if the post is not found, and log an error if that's the case. It shouldn't happen and may point to other problems in the load-test.

@agnivade
Copy link
Member

Both Alejandro and Claudio are out. I will take another quick look and ask for a stamp approval.

@agnivade
Copy link
Member

I gave it a test run and found some minor issues. We should be good to go after that.

1. Incorrect error/info logging when there are no scheduled posts.

Log line is:

error [2024-12-23 14:51:17.856 +05:30] control/simulcontroller.(*SimulController).sendScheduledPostNow loadtest/control/simulcontroller/scheduled_posts.go:135 no scheduled posts available caller="loadtest/loadte
st.go:60" controller_id=4 user_id=anoxyyicofbozjrig7og95i6py
...
error [2024-12-23 14:51:34.667 +05:30] control/simulcontroller.(*SimulController).deleteScheduledPost loadtest/control/simulcontroller/scheduled_posts.go:113 no scheduled posts available caller="loadtest/loadtes
t.go:60" controller_id=10 user_id=bpjjn1f67jg8xnya68m41ki6dc

Note that the log level is error, but it's just a case of scheduled posts not being available. And we are correctly doing it here:

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"}
}

But the issue is that GetRandomScheduledPost() already returns an error if no scheduled posts are found.

The right way is to return an error variable and check for it. See for example RandomTeam:

	if len(teams) == 0 {
		return model.Team{}, ErrTeamStoreEmpty
	}

and then we check for the error at call site:

team, err := u.Store().RandomTeam(store.SelectMemberOf | store.SelectNotCurrent)
if errors.Is(err, memstore.ErrTeamStoreEmpty) {
return control.UserActionResponse{Info: "no other team to switch to"}
} else if err != nil {
return control.UserActionResponse{Err: control.NewUserError(err)}
}

We should apply the same logic while calling GetRandomScheduledPost as well.

2. Invalid scheduled at time:

error [2024-12-23 14:52:18.420 +05:30] control/simulcontroller.(*SimulController).createScheduledPost loadtest/control/simulcontroller/scheduled_posts.go:63 Invalid scheduled at time., id=m9txr5p37bf63kg4xwq7upjxcy caller="loadtest/loadtest.go:60" controller_id=9 user_id=b1ysdcjc5prodfef1386jdjawy

This is what I see in the server logs:

debug [2024-12-23 15:01:16.543 +05:30] Invalid scheduled at time.                    caller="web/context.go:120" path=/api/v4/posts/schedule request_id=xzx84obdgjgdugma4setb8a1ye ip_addr=127.0.0.1 user_id=eh1k7cuhmtntfx8y9ziihatmkr method=POST err_where=ScheduledPost.IsValid http_code=400 error="ScheduledPost.IsValid: Invalid scheduled at time., id=qoafc3rp3tf5zjsqpcck9ew55w"

If we fix the above 2 issues, we should be good to go.

@agarciamontoro
Copy link
Member

Thanks for taking a look, @agnivade!

@harshilsharma63
Copy link
Member Author

I'll address the change next week and I'm getting the edit file functionality ready for feature complete by Monday

@agarciamontoro
Copy link
Member

Got it, @harshilsharma63, and thank you for your work here!

@agnivade
Copy link
Member

@harshilsharma63 - Just checking to see the status on this. I think there are just a few minor things pending, and then we should be good to merge this. So hopefully not a lot of effort required.

@harshilsharma63
Copy link
Member Author

@harshilsharma63 - Just checking to see the status on this. I think there are just a few minor things pending, and then we should be good to merge this. So hopefully not a lot of effort required.

I'm working to finish scheduled posts on mobile before the feature complete date so have deferred this until that. Should be done in Feb first half.

@agnivade
Copy link
Member

Thanks. Ideally, we should not be releasing features which are not load tested. @streamer45 - wondering if you can help bump up the priority on this so that Harshil is free to wrap up the work?

Copy link
Contributor

@streamer45 streamer45 left a comment

Choose a reason for hiding this comment

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

Looks good overhaul. Left mostly minor suggestions.

One additional comment is that I don't see any websocket addition here, and wondering how we are keeping the state in sync across multiple clients (e.g. deleting a draft).

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, removed.

Comment on lines 27 to 28
post, err := u.Store().RandomPostForChannel(channel.Id)
if err == nil {
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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, but this doesn't matter as all thats happening from isReply param is changing the avg word length, nothing else The part that send scheduled post as a reply is above where I set the rootId. But fixed this anyways as it is now semantically correct.

}
}

func TestRandomFutureTimeZeroDuration(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I'd expect these to be subcases of TestRandomFutureTime rather than top-level tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

Comment on lines 21 to 23
if randomTime < start.Unix() || randomTime > end.Unix() {
t.Errorf("RandomFutureTime() = %v, want between %v and %v", randomTime, start.Unix(), end.Unix())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We may want to use the require package for these checks.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

Comment on lines +93 to +95
// RandomFutureTime returns a random Unix timestamp, in milliseconds, in the interval
// [now+deltaStart, now+deltaStart+maxUntil]
func RandomFutureTime(deltaStart, maxUntil time.Duration) int64 {
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 find it more versatile if we returned a time.Time and then let the caller format it as needed. Otherwise, if we can only foresee a timestamp usage, we could rename this to reflect its behavior better.

Copy link
Member Author

Choose a reason for hiding this comment

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

There's just one use case of this function right now and I don't want to spend excessive time refactoring this to accommodate all possible cases. This is fine for now can can be updated later as needed. I've already refactored this twice.

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 :)

Copy link
Member Author

Choose a reason for hiding this comment

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

We'd end up creating constants for every day. Skipping this for now.

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

While there is no explicit check for it, just like in other random functions, I am removing scheduled posts from store when deleting or posting them, so it will not return a deleted scheduled post.

@agnivade
Copy link
Member

@harshilsharma63 - Just wanted to check on this. Would you be able to find some time to address the review comments?

@harshilsharma63
Copy link
Member Author

@agnivade will do this tomorrow. Noted.

@harshilsharma63
Copy link
Member Author

@agnivade I've fixed the comments and invalid time error.

@agnivade agnivade requested a review from streamer45 February 20, 2025 16:14
@agnivade
Copy link
Member

Thanks!

@streamer45
Copy link
Contributor

@harshilsharma63 Thoughts on my top comment above? On the websocket part.

@harshilsharma63
Copy link
Member Author

harshilsharma63 commented Feb 21, 2025

One additional comment is that I don't see any websocket addition here, and wondering how we are keeping the state in sync across multiple clients (e.g. deleting a draft).

@agnivade @streamer45 I didn't know we had to do it. Can you share an example where this is being done and I'll refer it? Can you also explain a bit more about this? This is the first time I'm adding something to load test so I don't know the conventions and what all stuff needs to be done. I followed a channel bookmark example.

@agnivade
Copy link
Member

Basically look at the simulcontroller/websocket.go file. We need to add event handlers for scheduled post events and keep the load-test agent state up to date. Although we don't do it for a lot of other events. But I agree that we should start doing it.

@streamer45
Copy link
Contributor

Yeah, the idea is to mimic a real client's behavior as much as possible including any synchronization logic that may happen as a result of receiving a websocket event. Usually, I'd expect a Redux handler to be in place on the web implementation, which on this side can be converted to use the local store. See how we handle the posted event as a practical example:

func (ue *UserEntity) handlePostEvent(ev *model.WebSocketEvent) error {
var data string
if el, ok := ev.GetData()["post"]; !ok {
return errors.New("post data is missing")
} else if data, ok = el.(string); !ok {
return fmt.Errorf("type of the post data should be a string, but it is %T", el)
}
var post *model.Post
if err := json.Unmarshal([]byte(data), &post); err != nil {
return err
}
switch ev.EventType() {
case model.WebsocketEventPosted, model.WebsocketEventPostEdited:
currentChannel, err := ue.store.CurrentChannel()
if err == nil && currentChannel.Id == post.ChannelId {
return ue.store.SetPost(post)
} else if err != nil && !errors.Is(err, memstore.ErrChannelNotFound) {
return fmt.Errorf("failed to get current channel from store: %w", err)
}
case model.WebsocketEventPostDeleted:
return ue.store.DeletePost(post.Id)
}
return nil
}

case model.WebsocketEventPosted:
post, err := getPostFromEvent(ev)
if err != nil {
c.status <- c.newErrorStatus(fmt.Errorf("failed to get post from event: %w", err))
break
}
if ack, ok := ev.GetData()["should_ack"]; ok && ack.(bool) {
if err := c.user.PostedAck(post.Id, "success", "", ""); err != nil {
c.status <- c.newErrorStatus(err)
break
}
}
cm, _ := c.user.Store().ChannelMember(post.ChannelId, c.user.Store().Id())
if cm.UserId != "" {
break
}
c.status <- c.newInfoStatus(fmt.Sprintf("channel member for post's channel missing from store, fetching: %q", post.ChannelId))
if err := c.user.GetChannelMember(post.ChannelId, c.user.Store().Id()); err != nil {
c.status <- c.newErrorStatus(fmt.Errorf("GetChannelMember failed: %w", err))
break
}
// If we were to follow webapp literally we'd have to check against user's
// preferences and possibly reload direct/group channels (and users) if
// necessary. However this only happens if the preferences for these
// channels are set not to show them, something we don't support yet as we default to
// showing them.

You can notice we have two main places to handle ws events, based on what needs to happen. If we just need to save the data to the store, then we modify the internal userentity/websocket.go file. If we need to execute some actions as a result of receiving the event, then we'd be touching the simulcontroller/websocket.go.

@agnivade
Copy link
Member

@streamer45 - what are your thoughts on splitting the ws event handler to another PR? This PR has been up for 4 months and quite big already. I am thinking if we could merge this and send another PR after this for the ws handler. That way atleast we start getting some feature coverage for scheduled posts.

@streamer45
Copy link
Contributor

@streamer45 - what are your thoughts on splitting the ws event handler to another PR? I am thinking if we could merge this and send another PR after this for the ws handler. That way atleast we start getting some feature coverage for scheduled posts.

Not opposed. Mostly I'd like to understand (from someone who developed the functionality) what ws events we need to handle and what the handling looks like. Do we make additional API requests in response? Is it just about saving data to the store?

These questions help me understand what the performance impact of leaving these out could be.

This PR has been up for 4 months and quite big already.

I'd encourage us to understand more about that (I have a few thoughts, e.g. the choice of 3 reviewers). Maybe a quick retro would be beneficial since we want to make this process as seamless as possible.

I'll remove myself from review to unblock and let @agarciamontoro do a final pass. Thanks for your patience :)

@streamer45 streamer45 removed their request for review February 25, 2025 14:30
@harshilsharma63
Copy link
Member Author

@streamer45

Mostly I'd like to understand (from someone who developed the functionality) what ws events we need to handle and what the handling looks like. Do we make additional API requests in response? Is it just about saving data to the store?

just to keep state updated, thats the only purpose of WS handling.

I'd encourage us to understand more about that (I have a few thoughts, e.g. the choice of 3 reviewers). Maybe a quick retro would be beneficial since we want to make this process as seamless as possible.

The PR has been up for 4 months because I started working on another task and couldn't stop working on it to get back to this.

@agnivade
Copy link
Member

I'd encourage us to understand more about that (I have a few thoughts, e.g. the choice of 3 reviewers).

To be clear, I wasn't explicitly looking for your review when I said the following 😛

@streamer45 - wondering if you can help bump up the priority on this so that Harshil is free to wrap up the work?

I was hoping you could have a chat with the leads so that Harshil gets enough bandwidth to prioritise this load-test work. Otherwise, he was constantly getting pulled into developing other features. But you did a review, so no harm done 😄

@agarciamontoro - could you give this a final pass? Thanks.

@harshilsharma63 - As Claudio outlined here: #848 (comment), we can set up event handlers for those ws events and update the store of the load-test agent to keep the state in sync. Let's schedule that work as a separate PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants