Skip to content

Default app design encourages data race #1290

@apapsch

Description

@apapsch

Using the cobra skeleton generator, root.go contains a global variable cfgFile, and the pointer is passed to rootCmd.PersistentFlags().StringVar(). This may result in data race in some circumstances. In my case it happened with go test -race -v and a table based test containing three sub tests.

=== RUN   TestExecution
=== RUN   TestExecution/successful_date
==================
WARNING: DATA RACE
Write at 0x000001513440 by goroutine 13:
  github.com/spf13/pflag.newStringValue()
      /home/ep/go/pkg/mod/github.com/spf13/[email protected]/string.go:7 +0x44
  github.com/spf13/pflag.(*FlagSet).StringVar()
      /home/ep/go/pkg/mod/github.com/spf13/[email protected]/string.go:37 +0x36
  gitlab.com/sterndata/cronjobd/cmd.NewRootCommand()
      /home/ep/src/monolith/go/cronjobd/cmd/root.go:29 +0x187
  gitlab.com/sterndata/cronjobd_test.TestExecution.func2()
      /home/ep/src/monolith/go/cronjobd/integration_test.go:62 +0x40e
  testing.tRunner()
      /usr/lib/go/src/testing/testing.go:1123 +0x202
[...]
[...]
==================
==================
WARNING: DATA RACE
Write at 0x00c00015c710 by goroutine 13:
  github.com/spf13/viper.(*Viper).SetConfigName()
      /home/ep/go/pkg/mod/github.com/spf13/[email protected]/viper.go:1931 +0x389
  github.com/spf13/viper.SetConfigName()
      /home/ep/go/pkg/mod/github.com/spf13/[email protected]/viper.go:1928 +0x357
  gitlab.com/sterndata/cronjobd/cmd.initConfig()
      /home/ep/src/monolith/go/cronjobd/cmd/root.go:62 +0x356
  github.com/spf13/cobra.(*Command).preRun()
      /home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:880 +0x81
  github.com/spf13/cobra.(*Command).execute()
      /home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:816 +0x1b5
  github.com/spf13/cobra.(*Command).ExecuteC()
      /home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:958 +0x4b2
  github.com/spf13/cobra.(*Command).ExecuteC()
      /home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:907 +0xaa8
  github.com/spf13/cobra.(*Command).Execute()
      /home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:895 +0x4d7
  github.com/spf13/cobra.(*Command).ExecuteContext()
      /home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:888 +0x4ce
  gitlab.com/sterndata/cronjobd_test.TestExecution.func2()
      /home/ep/src/monolith/go/cronjobd/integration_test.go:65 +0x48d
  testing.tRunner()
      /usr/lib/go/src/testing/testing.go:1123 +0x202

I worked around the problem by removing cfgFile, func initConfig and func init from root.go. CLI flags suffice for me, so that fits the bill.

The snake could be defanged by removing the need for global variables and func inits. In my app I replaced rootCmd and the other *cobra.Command globals with NewXXXCommand functions:

func NewRootCommand() *cobra.Command {
	return &cobra.Command
                // ...
	}
}
func NewServeCommand(parent *cobra.Command) *cobra.Command {
	cmd := &cobra.Command{
		// ...
	}
	parent.AddCommand(cmd)
	// cmd.Flags().String(...)
	return cmd
}

This requires main to grow a bit:

func main() {
	rootCmd := cmd.NewRootCommand()
	cmd.NewRunCommand(rootCmd)
	cmd.NewServeCommand(rootCmd)
	cmd.Execute(rootCmd)
}

It improves testability, because I can construct the commands in my test file without worrying about global state scattered across a few files.

func TestExecution(t *testing.T) {
	rootCmdForServe := cmd.NewRootCommand()
	rootCmdForServe.SetArgs([]string{"serve"})
	rootCmdForServe.SetErr(ioutil.Discard)
	serveCmd := cmd.NewServeCommand(rootCmdForServe)
	go func() {
		err := serveCmd.ExecuteContext(...)
		if err != nil {
			t.Fatalf("Failed executing serve command: %v", err)
		}
	}()
        // ...
}

This approach has the downside that main needs to grow with each command, because NewXXXCommand adds the child command to the parent. One might argue, adding child to parent needs to happen anyway, and it is better to do it in one function than scatter it across many init functions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/cobra-commandCore `cobra.Command` implementationslifecycle/needs-prReady for a PR from the communitytriage/needs-infoNeeds more investigation from maintainers or more info from the issue provider

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions