MM-17489: fixed racy tryExecutePluginCommand (#11780)
* MM-17489: fixed racy tryExecutePluginCommand * PR feedback * MM 7971 dockerhost -> localhost && docker -> docker-compose (#10872) * replace dockerhost with localhost * remove uneeded setup-max build step (no more dockerhost) * changes as recommended by @cpanato * make clean-docker with docker-compose * added ports to docker-compose.yml (needed for osx). ignore error for ldapadd (when already exists) * add clean-old-docker to legacy.mk * docker-compose stop instead of down for `make stop-docker` * MM-17117: Updates einterface for FirstLoginSync. (#11737) * update sampledata users passwords (#11769) * update sampledata users passwords * update makefile * Tweak docker compose setup (#11785) * fix stray dockerhost mention * fix stray whitespace * fix missing mattermost-redix cleanup in clean-old-docker * absolute path to docker-compose, plus simpler ldap initialization * allow ldapadd to fail on CI * MM-16366 - Prevent attachment duplication on import (#11770) * prevent attachment duplication * improved tests * corrected import into a new post * WIP: a failed attempt to add tests * remove debugging statements * fix godoc * enable plugins when testing * rework reentrant unit tests
Этот коммит содержится в:
@@ -5,6 +5,7 @@ package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
@@ -46,7 +47,7 @@ func TestPluginCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
type MyPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
plugin.MattermostPlugin
|
||||
|
||||
configuration configuration
|
||||
}
|
||||
@@ -110,6 +111,110 @@ func TestPluginCommand(t *testing.T) {
|
||||
th.App.RemovePlugin(pluginIds[0])
|
||||
})
|
||||
|
||||
t.Run("re-entrant command registration on config change", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.Plugins["testloadpluginconfig"] = map[string]interface{}{
|
||||
"TeamId": args.TeamId,
|
||||
}
|
||||
})
|
||||
|
||||
tearDown, pluginIds, activationErrors := SetAppEnvironmentWithPlugins(t, []string{`
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
type configuration struct {
|
||||
TeamId string
|
||||
}
|
||||
|
||||
type MyPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
|
||||
configuration configuration
|
||||
}
|
||||
|
||||
func (p *MyPlugin) OnConfigurationChange() error {
|
||||
p.API.LogInfo("OnConfigurationChange")
|
||||
err := p.API.LoadPluginConfiguration(&p.configuration);
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.API.LogInfo("About to register")
|
||||
err = p.API.RegisterCommand(&model.Command{
|
||||
TeamId: p.configuration.TeamId,
|
||||
Trigger: "plugin",
|
||||
DisplayName: "Plugin Command",
|
||||
AutoComplete: true,
|
||||
AutoCompleteDesc: "autocomplete",
|
||||
})
|
||||
if err != nil {
|
||||
p.API.LogInfo("Registered, with error", err, err.Error())
|
||||
return err
|
||||
}
|
||||
p.API.LogInfo("Registered, without error")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) ExecuteCommand(c *plugin.Context, commandArgs *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
p.API.LogInfo("ExecuteCommand")
|
||||
// Saving the plugin config eventually results in a call to
|
||||
// OnConfigurationChange. This used to deadlock on account of
|
||||
// effectively acquiring a RWLock reentrantly.
|
||||
err := p.API.SavePluginConfig(map[string]interface{}{
|
||||
"TeamId": p.configuration.TeamId,
|
||||
})
|
||||
if err != nil {
|
||||
p.API.LogError("Failed to save plugin config", err, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
p.API.LogInfo("ExecuteCommand, saved plugin config")
|
||||
|
||||
return &model.CommandResponse{
|
||||
ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL,
|
||||
Text: "text",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&MyPlugin{})
|
||||
}
|
||||
`}, th.App, th.App.NewPluginAPI)
|
||||
defer tearDown()
|
||||
|
||||
require.Len(t, activationErrors, 1)
|
||||
require.Nil(t, nil, activationErrors[0])
|
||||
|
||||
wait := make(chan bool)
|
||||
killed := false
|
||||
go func() {
|
||||
defer close(wait)
|
||||
|
||||
resp, err := th.App.ExecuteCommand(args)
|
||||
|
||||
// Ignore if we kill below.
|
||||
if !killed {
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, model.COMMAND_RESPONSE_TYPE_EPHEMERAL, resp.ResponseType)
|
||||
require.Equal(t, "text", resp.Text)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-wait:
|
||||
case <-time.After(10 * time.Second):
|
||||
killed = true
|
||||
}
|
||||
|
||||
th.App.RemovePlugin(pluginIds[0])
|
||||
if killed {
|
||||
t.Fatal("execute command appears to have deadlocked")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error after plugin command unregistered", func(t *testing.T) {
|
||||
_, err := th.App.ExecuteCommand(args)
|
||||
require.NotNil(t, err)
|
||||
|
||||
Ссылка в новой задаче
Block a user