diff options
author | Patrick Steinhardt <psteinhardt@gitlab.com> | 2023-08-21 13:37:52 +0300 |
---|---|---|
committer | Patrick Steinhardt <psteinhardt@gitlab.com> | 2023-09-05 08:40:40 +0300 |
commit | 5c9ed5a5d20b0c93beac30a446a5e1b1394d3dce (patch) | |
tree | 63e214433fee40eaf0cfbde97ea53e5d8e32f854 | |
parent | fea96d943f4948e7fe1777904668a6acc02ed375 (diff) |
log: Work around race when replacing gRPC logger
The `log.Configure()` function is expected to be called exactly once,
either in the `main()` function or in the respective subcommand. This is
because we set up global resources, namely our normal loggers as well as
the gRPC logger. Consequentially, when multiple tests call that function
in parallel, we run into a data race that gets detected by Go's race
detector.
While our normal loggers will eventually be converted to be non-global,
the gRPC logger cannot ever be non-global because the gRPC library does
not provide a way to do so. We thus introduce an ugly workaround in the
form of a new variable that asks the `log` package to skip replacing the
gRPC logger, which is only set in our `testhelper` package.
While not pretty, it at least addresses the data race.
-rw-r--r-- | internal/log/logger.go | 25 | ||||
-rw-r--r-- | internal/testhelper/configure.go | 5 |
2 files changed, 24 insertions, 6 deletions
diff --git a/internal/log/logger.go b/internal/log/logger.go index ef8604b3f..7aacc409f 100644 --- a/internal/log/logger.go +++ b/internal/log/logger.go @@ -50,6 +50,17 @@ var defaultLogger = func() *logrus.Logger { return logger }() +// SkipReplacingGlobalLoggers will cause `Configure()` to skip replacing global loggers. This is mostly a hack: command +// line applications are expected to call `log.Configure()` in their subcommand actions, and that should indeed always +// replace global loggers, as well. But when running tests, we invoke the subcommand actions multiple times, which is +// thus re-configuring the logger repeatedly. Because global logger are per definition a global shared resource, the +// consequence is that we might end up replacing the global loggers while tests are using them, and this race rightfully +// gets detected by Go's race detector. +// +// This variable should thus only be set in the testhelper's setup routines such that we configure the global logger a +// single time for all of our tests, only. +var SkipReplacingGlobalLoggers bool + // Config contains logging configuration values type Config struct { Dir string `toml:"dir,omitempty" json:"dir"` @@ -64,14 +75,16 @@ func Configure(out io.Writer, format string, level string, hooks ...logrus.Hook) return nil, err } - // We replace the gRPC logger with a custom one because the default one is too chatty. - grpcLogger := logrus.New() //nolint:forbidigo + if !SkipReplacingGlobalLoggers { + // We replace the gRPC logger with a custom one because the default one is too chatty. + grpcLogger := logrus.New() //nolint:forbidigo - if err := configure(grpcLogger, out, format, mapGRPCLogLevel(level), hooks...); err != nil { - return nil, err - } + if err := configure(grpcLogger, out, format, mapGRPCLogLevel(level), hooks...); err != nil { + return nil, err + } - grpcmwlogrus.ReplaceGrpcLogger(grpcLogger.WithField("pid", os.Getpid())) + grpcmwlogrus.ReplaceGrpcLogger(grpcLogger.WithField("pid", os.Getpid())) + } return Default(), nil } diff --git a/internal/testhelper/configure.go b/internal/testhelper/configure.go index 537485239..7788788a3 100644 --- a/internal/testhelper/configure.go +++ b/internal/testhelper/configure.go @@ -86,6 +86,11 @@ func configure() (_ func(), returnedErr error) { return nil, fmt.Errorf("configuring logger failed: %w", err) } + // From now on we will refrain from replacing the gRPC logger. The gRPC logger is a global shared resource, so + // any tests that run in parallel and end up calling `log.Configure()` would potentially try to replace the + // logger while it is in use. + log.SkipReplacingGlobalLoggers = true + for key, value := range map[string]string{ // We inject the following two variables, which instruct Git to search its // configuration in non-default locations in the global and system scope. This is |